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 replimport Json.Decode as JsJs.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" kleinzuschreibenOk False : Result Js.Error BoolJs.decodeString Js.string "\"ein String im String\""-- der Elm-String muss als Inhalt einen Json-String enthalten; daher die inneren QuotesOk ("ein String im String") : Result Js.Error StringJs.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) jsonStringOk 80 : Result Js.Error IntJs.decodeString (Js.field "hostname" Js.string) jsonStringOk "www.hszg.de" : Result Js.Error StringJs.decodeString (Js.field "portnumber" Js.int) jsonStringErr (Failure ("Expecting an OBJECT with a field named `portnumber`") <internals>): Result Js.Error IntJs.decodeString (Js.field "hostname" Js.int) jsonStringErr (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 -> HosthostBuilder hname pnum ={hostname = hname, portnumber = pnum}<function> : String -> Int -> HosthostDecoder = Js.map2 hostBuilder (Js.field "hostname" Js.string) (Js.field "port" Js.int)<internals> : Js.Decoder HostJs.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