Softwerkskammer

 

Kapitel 05

Hier folgt die Zusammenfassung der ersten Hälfte des fünften Kapitels von "Real World Haskell".

Writing a Library: Working with JSON Data

  • Teil 1 bis "Pretty Printing a String" (bis etwa Seite 121)

Agenda

  1. JSON
  2. Module
  3. SimpleJSON Renderer
  4. Linking
  5. Prettify Bibliothek

JSON

  • JSON Sprache ist einfache Repräsentation
  • zum Speichern und Übertragen von strukturierten Daten
  • Datentransfer von Webservice zu Javascript-Anwendung (Browser)
  • siehe

JSON Typen

Basistypen

  • String - "a string"
  • Number - 12345
  • Boolean - true
  • null - null

Zusammengesetzte Typen

  • Array - unsortierte Sequenz von Werten
    • [1, "foobar", true, null]
  • Object - unsortierte Sammlung von Schlüssel/Werte-Paaren:
    • {"a":[1, 2, 3], "testable": false}

Repräsentation in Haskell

  • algebraischer Datentyp
  • je JSON-Typ ein Value Constructor
    1
    2
    3
    4
    5
    6
    7
    data JValue = JString String
              | JNumber Double
              | JBool Bool
              | JNull
              | JObject [(String, JValue)]
              | JArray [JValue]
                deriving (Eq, Ord, Show)```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
ghci> :load SimpleJSON
[1 of 1] Compiling SimpleJSON ( SimpleJSON.hs, interpreted )
Ok, modules loaded: SimpleJSON.
ghci> JString "foo"
JString "foo"
ghci> JNumber 2.7
JNumber 2.7
ghci> :type JBool True
JBool True :: JValue```
 
---
 
## Pattern Matching zum Lesen von JSON
```haskell
getString :: JValue -> Maybe String
getString (JString s) = Just s
getString _ = Nothing```
 
```haskell
ghci> :reload
Ok, modules loaded: SimpleJSON.
ghci> getString (JString "hello")
Just "hello"
ghci> getString (JNumber 3)
Nothing```
 
---
 
## Weitere Zugriffsfunktionen
```haskell
getInt (JNumber n) = Just (truncate n)
getInt _ = Nothing
 
getDouble (JNumber n) = Just n
getDouble _ = Nothing
 
getBool (JBool b) = Just b
getBool _ = Nothing
 
getObject (JObject o) = Just o
getObject _ = Nothing
 
getArray (JArray a) = Just a
getArray _ = Nothing
 
isNull v = v == JNull```
 
---
## Tipps am Rande
 
* Reload von *.hs
 
```haskell
ghci> :load SimpleJSON.hs
[1 of 1] Compiling Main             ( SimpleJSON.hs, interpreted )
Ok, modules loaded: Main.
ghci> :reload
Ok, modules loaded: Main.
  • Nachkommastellen abschneiden
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ghci> truncate 5.8
5
ghci> :module +Data.Ratio
ghci> truncate (22 % 7)
3```
 
---
 
## Haskell Module
 
* je Quellcode-Datei eine Moduldefinition (am Anfang der Datei stehen)
 
```haskell
module SimpleJSON
(
JValue(..)
, getString
, getInt
, getDouble
, getBool
, getObject
, getArray
, isNull
) where
 
data JValue = JString String
            | JNumber Double
            ...
  • module ist reserviertes Wort, Modulname == Dateiname (ohne Endung)
  • Liste von Exporten in runden Klammern => sichtbar für andere Module
  • privater Code bleibt von Außenwelt versteckt
  • where leitet Modul-Körper ein

Export

Export von Type und Value Constructor

  • JValue(..) .. exportiert Typ JValue und alle Value Constructor
  • auch nur Typ exportierbar (ohne Value Constructor)
    • abstrakter Typ
    • Details eines Typs vor Benutzer verstecken
    • ohne Value Constructor kein Pattern Matching und keine Werterzeugung

Alles exportieren

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
module ExportEverything where```
 
### Nichts exportieren (selten sinnvoll)
```haskell
module ExportNothing () where```
 
---
 
## Haskell Modul kompilieren
```haskell
ghc -c SimpleJSON.hs```
 
* -c: nur  Objekt-Code
  * ohne -c würde es fehlschlagen weil main Funktion fehlt (unter Windows kein Problem)
* SimpleJSON.hi: Interface-Datei, Informationen über exportierte Namen des Moduls
* SimpleJSON.o: Objekt-Datei, enthält Maschinencode
 
---
 
## Module importieren
 
```haskell
module Main where
import SimpleJSON
main = print (JObject [("foo", JNumber 1), ("bar", JBool False)])```
 
* Beispiel aus Buch funktioniert nicht: `module Main () where`
```haskell
Main.hs:1:1:
    The main function `main' is not exported by module `Main'```
 
* import muss am Dateianfang in einem Block mit anderen import-Anweisungen stehen
* import `Modulname` importiert alle vom Modul exportierte Namen
 
---
 
## Linking - Ausführbare Dateien generieren
 
```haskell
ghc -o simple Main.hs
Linking simple.exe ...```
 
* bei Angabe von `SimpleJSON.o` kommen Fehler
 
```haskell
ghc -o simple Main.hs SimpleJSON.o
Linking simple.exe ...
SimpleJSON.o:fake:(.data+0x0): multiple definition of `__stginit_SimpleJSON'
...```
 
* eigentlich explizit alle Dateien übergeben, die in der EXE landen sollen
 
* Mischen von *.hs und *.o Dateien beim Linking
* ghc kompiliert nur wenn nötig
 
---
 
## JSON Daten ausgeben (einfache Variante)
 
```haskell
module PutJSON where
 
import Data.List (intercalate)
import SimpleJSON
 
renderJValue :: JValue -> String
renderJValue (JString s) = show s
renderJValue (JNumber n) = show n
renderJValue (JBool True) = "true"
renderJValue (JBool False) = "false"
renderJValue JNull = "null"
 
renderJValue (JObject o) = "{" ++ pairs o ++ "}"
  where pairs [] = ""
        pairs ps = intercalate ", " (map renderPair ps)
        renderPair (k,v) = show k ++ ": " ++ renderJValue v
 
renderJValue (JArray a) = "[" ++ values a ++ "]"
  where values [] = ""
        values vs = intercalate ", " (map renderJValue vs)
 
putJValue :: JValue -> IO ()
putJValue v = putStrLn (renderJValue v)```
 
---
 
## Separieren von puren und nicht puren Code
 
* mächtiges und weit verbreitetes Vorgehen in Haskell
 
* renderJValue gibt einfach String zurück
 
* putJValue gibt String auf Konsole aus (I/O)
 
* erhöht Flexibilität (z. B. Einfügen von Kompression)
 
---
 
## Zweischneidiges Schwert Typ-Inferenz
 
* Typ-Inferenz vom Haskell-Compiler ist mächtig und nützlich
* aber Gefahr, sich zu sehr auf Compiler zu verlassen
  * Typ-Informationen einfach weglassen
  * den Compiler den Typ ermitteln lassen
 
* Compiler wird möglicherweise schlüssige und konsitente Lösung finden
  * möglicherweise aber nicht, was Programmierer gemeint hat
  * Fehlersuche schwierig, weil Auftreten ungleich Ursache
 
---
 
## Beispiel problematische Typ-Inferenz
 
```haskell
-- Typsignatur weggelassen, man denkt, es wird ein String zurückgeliefert
upcaseFirst (c:cs) = toUpper c -- Vergessen ":cs" anzuhängen
-- aber Kompiler inferiert String -> Char
 
-- Wiederverwendung in anderer Funktion
camelCase :: String -> String
camelCase xs = concat (map upcaseFirst (words xs))
 
-- Fehler irreführend, Meldung bei Verwendung von upcaseFirst
Couldn't match expected type `[Char]' against inferred type `Char'
Expected type: [Char] -> [Char]
Inferred type: [Char] -> Char
In the first argument of `map', namely `upcaseFirst'
In the first argument of `concat', namely
`(map upcaseFirst (words xs))'
Failed, modules loaded: none.```
 
* jede Typsignatur verringert Fehlentscheidungen des Typ-Inferenz-Mechanismus
* Typsignaturen auch für den Leser hilfreich
 
---
 
## Tipps Typdeklarationen
* man muss nicht jedes kleine Codefragement mit Typ-Deklaration versehen
* sinnvollerweise sollte es je Top-Level-Definition eine Typsignatur geben
* lieber am Anfang ein paar Typ-Signaturen mehr explizit hinschreiben
 
---
 
## Allgemeiner Rendering-Ansatz
 
* aktuelles JSON-Rendering genau zugeschnitten auf die vorhandenen Datentypen und die JSON Format-Konventionen
* Ausgabe ist nicht so gut lesbar
* Ziel: Rendering als generische Aufgabe
  * wie kann man eine Bibliothek bauen, die Daten sinnvoll für verschiedenste Situationen rendern kann
 
* Ausgabe sollte menschenlesbar und durch Maschinen verarbeitbar sein
* solche Bibliotheken heißen Pretty Printers
* obwohl es schon diverse Haskell-Pretty-Printer-Libraries gibt, entwicklen wir eine eigene: *Prettify*
 
---
 
## Zunächst neuer JSON Renderer
 
* nutzt noch nicht implementierte Prettify-API
* Prettify definiert abstrakten Typ `Doc`
 
```haskell
-- file: ch05/PrettyJSON.hs
renderJValue :: JValue -> Doc
renderJValue (JBool True) = text "true"
renderJValue (JBool False) = text "false"
renderJValue JNull = text "null"
renderJValue (JNumber num) = double num
renderJValue (JString str) = string str```
 
* die Funktionen text, double und string wird Prettify definieren
 
---
 
## Haskell Entwicklungs-Tipps
 
* Code während des Schreibens immer wieder Komplieren
* liefert gewisse Sicherheit durch Haskells starke Typisierung und Type Inferenz
 
* für die Entwicklung eines Programmgerüsts auf Stellvertreter (Stubs/Placeholder) setzen
* wir brauchen Stubs für `Doc`, `text`, `double`, `string`
  * werden erst durch Prettify zur Verfügung gestellt
  * bis dahin würde der Code nicht kompilieren
 
```haskell
-- file: ch05/PrettyStub.hs
import SimpleJSON
data Doc = ToBeDefined
           deriving (Show)
string :: String -> Doc
string str = undefined
text :: String -> Doc
text str = undefined
double :: Double -> Doc
double num = undefined```
 
---
 
## undefined
 
* spezieller Wert `undefined` hat den Typ `a`
* Typprüfung erfolgreich, aber bei Ausführung kracht es
 
```haskell
ghci> :type undefined
undefined :: a
ghci> undefined
*** Exception: Prelude.undefined
ghci> :type double
double :: Double -> Doc
ghci> double 3.14
*** Exception: Prelude.undefined```
 
* wir können den Code zwar nicht laufen lassen, aber die Typprüfung ist immer erfolgreich
 
---
 
## Pretty Print von einem String
 
* String in JSON ist eine Serie von Zeichen umhüllt von Anführungszeichen
 
```haskell
-- file: ch05/PrettyJSON.hs
string :: String -> Doc
string = enclose '"' '"' . hcat . map oneChar```
 
* `enclose` Funktion packt ein `Doc` Wert in öffende und schließende Zeichen ein
 
```haskell
-- file: ch05/PrettyJSON.hs
enclose :: Char -> Char -> Doc -> Doc
enclose left right x = char left <> x <> char right```
 
* `(<>)` Funktion hängt zwei `Doc`-Werte aneinander (wie `(++)`)
 
```haskell
-- file: ch05/PrettyStub.hs
(<>) :: Doc -> Doc -> Doc
a <> b = undefined

  • Funktion char nicht erklärt (wandelt Character in Doc um)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
char :: Char -> Doc
char c = undefined```
 
* Funktion `hcat` verbindet mehrere `Doc` Werte zu einem (analog concat für Listen)
 
```haskell
-- file: ch05/PrettyStub.hs
hcat :: [Doc] -> Doc
hcat xs = undefined```
 
* Funktion `string` führt `oneChar` Funktion für jedes Zeichen im String aus
* verbindet alles und umschliesst das Ergebnis in Anführungsstrichen
* oneChar rendert oder escapes einzelnes Zeichen
 
```haskell
-- file: ch05/PrettyJSON.hs
oneChar :: Char -> Doc
oneChar c = case lookup c simpleEscapes of
            Just r -> text r
            Nothing | mustEscape c -> hexEscape c
                    | otherwise -> char c
    where mustEscape c = c < ' ' || c == '\x7f' || c > '\xff'
 
simpleEscapes :: [(Char, String)]
simpleEscapes = zipWith ch "\b\n\f\r\t\\\"/" "bnfrt\\\"/"
    where ch a b = (a, ['\\',b])```
 
---
 
* `simpleEscapes` Wert ist eine Liste von Paaren
* genannt 'association list' oder 'alist'
* Verbindung zw. Zeichen und escaped Repräsentation
 
```haskell
ghci> take 4 simpleEscapes
[('\b',"\\b"),('\n',"\\n"),('\f',"\\f"),('\r',"\\r")]```
 
* Suche, ob Zeichen in `alist` enthalten ist, wird dann escaped
* nur druckbare ASCII-Zeichen werden unescaped ausgegeben
 
* Umwandlung eines Zeichens in Unicode-String `\u1234`
 
```haskell
-- file: ch05/PrettyJSON.hs
smallHex :: Int -> Doc
smallHex x = text "\\u"
           <> text (replicate (4 - length h) '0')
           <> text h
    where h = showHex x ""```
 
---
 
* `showHex` Funktion kommt von Numeric Bibliothek (muss importiert werden)
 
```haskell
ghci> showHex 114111 ""
"1bdbf"```
 
* `replicate` Funktion wird von `Prelude` bereitgestellt und erzeugt ein Liste mit immer dem gleichen Element
 
```haskell
ghci> replicate 5 "foo"
["foo","foo","foo","foo","foo"]```
 
---
 
## Point-Free-Style
 
```haskell
-- file: ch05/PrettyJSON.hs
string :: String -> Doc
string = enclose '"' '"' . hcat . map oneChar```
 
* Stil für das Schreiben von Funktionsdefinitionen als Komposition von anderen Funktionen
* nichts mit dem '.' zu tun (für Funktionskomposition)
* 'Point' meint 'Value'
  * point-free läßt die Values weg, auf denen operiert wird
 
* "pointy"-Version:
  * Variable `s` die den Wert referenziert, auf dem gearbeitet wird
 
```haskell
-- file: ch05/PrettyJSON.hs
pointyString :: String -> Doc
pointyString s = enclose '"' '"' (hcat (map oneChar s))```

 

Wiki Links

Europaweite Veranstaltungen 2019
Jahreskalender von Konferenzen, Open Spaces, BarCamps und anderen Veranstaltungen