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
data JValue = JString String
            | JNumber Double
            | JBool Bool
            | JNull
            | JObject [(String, JValue)]
            | JArray [JValue]
              deriving (Eq, Ord, Show)```

```haskell
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
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

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)
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))```