8. Elm - Eine funktionale Programmiersprache zur Entwicklung von Web-Apps
8.5 Json in Elm
Json in Elm
Wenn Sie mit einem Server Daten austauschen, dann ist Json ein übliches Format. Der Server könnte zum Beispiel eine Hochschul-Datenbank sein, und die Daten könnten so aussehen:
"student182952" : { "Immatrikulationsnummer" : 182952, "Jahrgang" : { "Semester" : "Winter", "Jahr" : "2022" }, "Name" : "Simone Schneider", "Kurse" : ["Algorithmen", "Netwerke", "Datenbanken"] }
In Javascript könnten wir das jetzt per Json.parse
in ein Objekt übersetzen.
Das Problem in Elm ist, dass der Datentyp bereits aus dem Code heraus
ersichtlich sein muss; es kann also gar keine allgemeine Funktion Json.parse
geben.
Dafür kann man sich recht problemlos aus Bausteinen einen Parser zusammenbauen.
elm repl
import Json.Decode as Js
Js.decodeString
<function> : Js.Decoder a -> String -> Result Js.Error a
Js.decodeString
wandelt also einen String
(das rohe Json)
in einen a
-Wert um, braucht aber einen "Json-nach-a
"-Decoder.
Am Besten zeige ich Ihnen ein paar Beispiele:
Js.decodeString Js.int "42"
Ok 42 : Result Js.Error Int
Warum ist der Output nicht 42
sondern Ok 42 : Result Js.Error Int
?
Offensichtlich kann Js.decodeString Js.int
nicht immer ein Int
liefern:
Js.decodeString Js.int "zwei"
Err (Failure ("This is not valid JSON! Unexpected token z in JSON at position 0") <internals>)
: Result Js.Error Int
Js.decodeString Js.int
wandelt den gegebenen (Json)-String also entweder
in ein Int
oder in eine Fehlermeldung um; hierfür brauchen wir einen Union-Typ,
in diesem Falle Result err value
. Dies ist ähnlich zu Maybe a
, nur
dass
es im Fehlerfall uns erlaubt, eine Fehlermeldungen als Payload anzuhängen.
Als zusammengesetzte Datentypen gibt es in Json Listen wieJs.decodeString Js.bool "false" -- achten Sie darauf, im Json-String "false" kleinzuschreiben
Ok False : Result Js.Error Bool
Js.decodeString Js.string "\"ein String im String\""-- der Elm-String muss als Inhalt einen Json-String enthalten; daher die inneren Quotes
Ok ("ein String im String") : Result Js.Error String
Js.decodeString Js.string "42"
Err (Failure ("Expecting a STRING") <internals>)
: Result Js.Error String
[2,3,5]
und Dictionaries
wie {hostname : "www.hszg.de", port : 80}
. Listen sind einfach zu decodieren.
Mit Js.list myJsonTo_a_decoder
erhalten sie einen
"Json-nach-(List a)-Decoder".
Js.decodeString (Js.list Js.int) "[42,6,78]"
Ok [42,6,78] : Result Js.Error (List Int)
Js.decodeString (Js.list Js.string) "[\"42\", \"hello\"]"
Ok ["42","hello"] : Result Js.Error (List String)
Beachten Sie: "[42,6,78]"
ist keine Liste aus Strings, sondern
ein String, dessen Inhalt wie eine Liste aussieht.
{"hostname" : "www.hszg.de", "port" : 80}
Mit Js.field "port"
erhalten Js.field "port" Js.int
erhalten wir einen Js.Decoder Int
, der aus dem Json-Dictionary
das Feld "port" herausziehen will; das funktioniert natürlich nur, wenn es
(1) das Feld gibt und (2) es tatsächlich ein Int ist.
jsonString = "{\"hostname\" : \"www.hszg.de\", \"port\" : 80}"
Js.decodeString (Js.field "port" Js.int) jsonString
Ok 80 : Result Js.Error Int
Js.decodeString (Js.field "hostname" Js.string) jsonString
Ok "www.hszg.de" : Result Js.Error String
Js.decodeString (Js.field "portnumber" Js.int) jsonString
Err (Failure ("Expecting an OBJECT with a field named `portnumber`") <internals>)
: Result Js.Error Int
Js.decodeString (Js.field "hostname" Js.int) jsonString
Err (Field "hostname" (Failure ("Expecting an INT") <internals>))
: Result Js.Error Int
Stellen Sie sich vor, wir hätten einen Datentyp host
wie folgt:
type alias Host =
{ hostname : String
, portnumber : Int
}
und wollen den String in ein Objekt vom Typ Host
umwandeln. Wir könnten
es per Hand machen und die Felder einzeln herausziehen und jedes Mal auf Fehler prüfen;
manchmal geht das nicht: gewisse Elm-Funktionen verlangen einen
"Json-nach-Datentyp-XYZ-Decoder". Hier kommen Js.map, Js.map2, ..., Js.map8
zur
Hilfe.
Js.map2
<function>: (a -> b -> value) -> Js.Decoder a -> Js.Decoder b -> Js.Decoder value
Die Semantik ist wie folgt: das zweite und dritte Argument sind ein Json-nach-a- bzw.
Json-nach-b-Decoder.
Das erste ist eine Funktion, die aus einem a
und einem b
ein
Objekt
vom Type value
bauen. Hier wollen wir aus String
und
Int
einen Host
bauen.
type alias Host = {hostname : String, portnumber : Int}
hostBuilder : String -> Int -> Host
hostBuilder hname pnum =
{hostname = hname, portnumber = pnum}
<function> : String -> Int -> Host
hostDecoder = Js.map2 hostBuilder (Js.field "hostname" Js.string) (Js.field "port" Js.int)
<internals> : Js.Decoder Host
Js.decodeString hostDecoder "{\"hostname\" : \"www.hszg.de\", \"port\" : 80}"
Ok { hostname = "www.hszg.de", portnumber = 80 }
: Result Js.Error Host
Ein bisschen Syntax-Zucker kommt uns zur Hilfe. Wir brauchen die Funktion
hostBuilder
gar nicht. Der Name der Typ-Alias
Host
selbst ist eine Funktion, die die Argumente in den Record-Typ
Host
umwandelt:
Host
<function> : String -> Int -> Host
Natürlich ist Host
keine "echte" Funktion, sondern ein Datenkonstruktor.
Es kann aber natürlich vorkommen, dass Sie als erstes Argument von
Js.map2
keinen Datenkonstruktor sondern eine echte Funktion
schreiben wollen:
hostDecoder = Js.map2 (\n p -> n ++ ":" ++ (String.fromInt p)) (Js.field "hostname" Js.string) (Js.field "port" Js.int)
Js.decodeString hostDecoder "{\"hostname\" : \"www.hszg.de\", \"port\" : 80}"
Gute Programmierpraxis ist das wahrscheinlich nicht; es ist wohl immer besser, die Daten erst
einmal
explizit in der Hand zu halten, bevor Sie sie weiterverarbeiten. Aber nach Syntax und
Semantik
von Elm
und Json.Decode
ist das gar kein Problem.
Übungsaufgabe Adaptieren Sie Website03ExpandList.elm, so dass oben eine große Textarea ist, in die ich im Json-Format eine Integer-Sequenz eingeben kann; diese soll aus einem Namen und einer Liste von ganzen Zahlen bestehen, wie zum Beispiel hier