8. Elm - Eine funktionale Programmiersprache zur Entwicklung von Web-Apps

8.4 HTTP-Requests

Bis jetzt hat unser Code passiv auf Nachrichten der Laufzeitumgebung gewartet. Manchmal wollen wir aber von selbst tätig werden und Nachrichten an die Umgebung schicken (z.B. Http-Requests). Diese "hinausgehenden" Nachrichten heißen im Elm-Kontext commands. Der entsprechende Datentyp ist Cmd msg, wobei msg eine Typenvariable ist: bei manchen Commands erwarten wir ja eine Antwort, die dann in Form einer Message hereinkommen wird.

Zufallszahlen

Einfachstes Beispiel, Commands zu illustrieren, ist eine Webseite, die auf Knopfdruck Zufallszahlen generiert. Zufallszahlen in die Hand zu bekommen ist in Elm erstaunlich schwierig. Es gibt keine Funktion Math.random() und kann sie auch nicht geben: Math.random() würde ja jedes Mal einen neuen Wert zurückliefern und wäre somit gar keine Funktion! Sie sehen, Elm nimmt es sehr sehr ernst mit dem funktionalen Paradigma. Wie kriegen wir also eine Zufallszahl? Starten Sie Website05Random.elm und schauen Sie sich den Code an. Auf Knopfdruck schickt die Laufzeitumgebung unserem Code die Message GenerateRandomNumber; soweit konzeptuell alles beim Alten. Sehen Sie sich aber Zeilen 45-47 an in der Funktion update an:

case msg of
        GenerateRandomNumber ->
            ( model, Random.generate NewRandomNumber (Random.float 0 1) )

Die Funktion update gibt also nicht nur das neue Modell zurück, sondern auch eine Anweisung an die Laufzeitumgebung. Versuchen wir den Ausdruck Random.generate NewRandomNumber (Random.float 0 1) zu entziffern:

  • Random.float 0 1 stellt einen Random.Generator Float her. Im Prinzip ist das eine Beschreibung der Art von Zufall, die wir wollen. Hier: ein Float gleichverteilt im Intervall [0,1].
  • NewRandomNumber. Dies ist eine Message in unserem Code. Besser gesagt ein Datenkonstruktor. Die Message selbst ist NewRandomNumber Float, sie trägt also ein Float als Payload.
  • Die Funktion Random.generate baut dann daraus einen Cmd.

Die Semantik ist, dass Zeile 47 einen Cmd erstellt und an die Laufzeit schickt; diese berechnet dann eine neue Zufallszahl im Intervall [0,1] und schickt sie als Payload der Message NewRandomNumber an unseren Code. Der kann dann innerhalb der Funktion update diese hereinkommende Nachricht weiterverarbeiten.

Http-Requests

An der Erzeugung von Zufallszahlen war überraschend, dass dies nicht innerhalb dies Elm-Codes geschehen konnte, sondern nur mithilfe der Umgebung zu bewerkstelligen war. Dies war natürlich dem strengen funktionalen Charakter von Elm geschuldet. Anders sieht es zum Beispiel bei Http-Requests aus: da liegt es in der Natur der Sache, dass sie nicht lokal im Code zu beantworten sind sondern, eventuell auch mit Zeitverzögerung, durch Kommunikation mit der Außenwelt. Das Comman`Message-Prinzip, das bei Zufallszahlen doch eher etwas gezwungen wirkte, kommt bei Http-Requests ganz natürlich ins Spiel.

In meiner "Minimalanwendung"

kann der Nutzer einen Suchbegriff eingeben und der Server antwortet dann etwa mit "Sie haben nach ... gesucht". Den Code finden Sie hier: Sehen wir uns die entscheidenden Codezeilen an.
SubmitButtonClicked ->
            ( { model | response = "" }
            , Http.get
                { url = "http://localhost:8001/search?" ++ "key=" ++ model.input
                , expect = Http.expectString ResponseReceived
                }
            )

Wir erstellen den Command mithilfe der Funktion Http.get. Diese erwartet zwei Argumente (technisch gesehen eins, das als Record wiederum zwei Werte enthält). Erstens den URL mitsamt Port; zweitens Instruktionen, was mit einer Antwort zu tun wäre. Die Idee ist, dass, wenn die Laufzeitumgebung einen Http-Response erhält, dieser an die Funktion ResponseReceived übergeben wird (typischerweise ist ResponseReceived ein Datenkonstruktur und keine echte Funktion), der daraus eine Message baut und diese dann in der Funktion update verarbeitet wird. Da bei Http-Requests aber Fehler auftreten können, ist der Datentyp des Responses aber kein String sondern ein Result Http.Error String. Dies ist ein Union-Datentyp ähnlich wie Result, und zwar

type Result err value                        
  = Error err
  | Ok value

Übungsaufgabe Ich habe meinem Login-Server 13-login-server.zip einen weiteren Endpunkt hinzugefügt: /fib-text; ein Request auf /fib-text?n=10 liefert das Ergebnis, hier also 55, als Text zurück und nicht als Json.

Schreiben Sie in Elm ein Fibonacci-Frontend. Der User eine Zahl eingeben. Klickt er auf den Knopf compute, dann schickt das Frontend einen Http-Request zum Login-Server. Wenn das Ergebnis (als String) empfangen wird, wird als Ergebnis auf der Seite ausgegeben.

Post-Requests

POST-Requests werden in Elm fast genau so behandelt wie GET-Requests. Die Funktion Http.post erwartet allerdings in ihrem Record-Argument noch einen weiteren Wert: body: Http.Body.

             Http.post
                { url = "http://localhost:8001/add"
                , body = Http.stringBody "application/json" ("{"key": "" ++ model.input ++ ""}")
                , expect = Http.expectString ResponseReceived
                }    
Das gesamte Code-Beispiel finden Sie hier:

Übungsaufgabe Schreiben Sie eine App (Client in Elm, Server in Node.js) aufbauend auf den vorherigen Code-Beispielen, wo der Server ein einfaches Dictionary verwaltet (zum Beispiel tatsächlich ein Wörterbuch Deutsch-Englisch). Der Client besteht aus zwei Seiten: auf der einen können Sie nach Wörtern suchen; auf der anderen können Sie ein Wortpaar wie Katze, cat hinzufügen.

Der Server sollte mindestens die zwei Endpunkte GET /find?word=... und POST /find?word=...&translation=... zur Verfügung stellen. Wenn Sie wollen, dürfen Sie auch eine delete-Funktion implementieren.