Tutorial: F# mit SAFE-Stack – Teil 3

Keine Kommentare

Eine Anwendung mit nur einer (funktionalen) Programmiersprache entwickeln

SAFE-Stack: Checkliste, teilweise abgehakt
(https://unsplash.com/photos/RLw-UC03Gwc)

Willkommen zurück zum dritten Teil der Serie.

Im ersten Teil der Serie haben wir das Grundgerüst für eine einfache To-do-Anwendung gebaut, die zunächst nur eine feste Liste von Todos anzeigen konnte. Im zweiten Teil haben wir sie dann erweitert, um neue To-dos hinzufügen zu können.

Die Anwendung ist vollständig in F# geschrieben und benutzt die Bibliotheken des SAFE-Stacks, insbesondere:

  • Giraffe (für die Bearbeitung von HTTP-Requests)
  • Fable (F# nach Javascript und React-UI-Elemente)
  • Elmish (für das Model-Update-View-Pattern)

In diesem Teil fügen wir der Anwendung eine neue, wichtige Funktion hinzu:

Wir wollen Aufgaben als erledigt markieren können.

Dazu soll das Backend einen neuen Service-Call anbieten und das Frontend diesen nutzen. Außerdem soll der Status einer Aufgabe angezeigt werden, sodass der Anwender diesen auch ändern kann.

Den Stand des Codes vom letzten Teil findet ihr hier.

Den vollständigen Code von heute findet ihr hier.

Los geht’s wieder einmal mit dem Frontend:

Index.fs

Wenn wir eine neue Funktionalität zur Verfügung stellen wollen, müssen wir wie immer dafür auch (mindestens) eine neue Message anbieten:

type Msg =

    ... // die bisherigen Messages

    | ToggleCompleted of int

Der Parameter soll die Id des Todos aufnehmen.

Das Model kann unverändert bleiben, ebenso die init-Funktion.

Aber natürlich brauchen wir für die neue Message eine Behandlung in der update-Funktion (der Compiler meckert auch schon):

let update msg model =
    match msg with

    ... // die bisherigen Messages

    | ToggleCompleted id ->
        let url = sprintf "/api/todos/%i/toggle_complete" id
        let newModel id = Fetch.patch<unit, Todo list> url
        let cmd = Cmd.OfPromise.either newModel () Refresh Error
        model, cmd

Im Backend werden wir gleich einen Service-Call anbieten, mit dem das »erledigt«-Flag eines Todos umgeschaltet wird. Dazu nutzen wir einen Endpunkt »unterhalb« von /api/todos, der die Id des zu ändernden Todos in der URL erwartet. Da wir ein bestehendes Datenobjekt ändern, verwenden wir das HTTP-Verb PATCH.

Als Erstes erzeugen wir einen URL-String mit der Id darin. Der Rest des Codes sieht ziemlich genauso aus wie die anderen Calls zum Backend.

Wir gehen davon aus, dass auch dieser Aufruf die neue, komplette Liste von Todos zurückliefert, wie das schon bei AddTodo der Fall ist.

User Interface

Da die Darstellung eines einzelnen Todos jetzt umfangreicher wird, lagern wir sie in eine eigene Funktion aus:

let todoView todo dispatch =
    div [] [
        input [
            Type "checkbox"
            Checked todo.Completed
            OnClick (fun _ -> todo.Id |> ToggleCompleted |> dispatch)
        ]
        label [ ] [str todo.Description]
    ]

Falls ihr diesen Code selbst tippt, werdet ihr zwischendurch möglicherweise Fehlermeldungen bekommen. Insbesondere, solange diese Zeile noch nicht da ist:

            Checked todo.Completed

Solange der Compiler nämlich nur sieht, dass wir von der Variablen todo die Eigenschaft Description nutzen (vorletzte Zeile), kann die type inference den Typ von todo nicht eindeutig erkennen. Das liegt daran, dass auch im Typentyp Model eine Eigenschaft Description definiert ist – was offensichtlich eine schlechte Wahl gewesen ist. Zum Glück ermöglicht es die Verwendung der Eigenschaft Completed, eindeutig den Typ von todo zu ermitteln. Andernfalls müsste man diesen Parameter typisieren ((todo: Todo)).

Ansonsten denke ich, dass der Code klar ist: Ein todo wird durch eine Checkbox und einen Text repräsentiert. Und an der Checkbox hängt ein OnClick-Handler, der die neue Message auslöst.

Die bereits bestehende Funktion todosView müssen wir natürlich noch umbauen, damit die neue Funktion zum Zeichnen eines To-dos auch aufgerufen wird:

let todosView model dispatch =
    div [] ( model.Todos |> List.map (fun each ->
        todoView each dispatch))

Das war’s auch schon im Frontend. Werfen wir einen Blick ins Backend:

Server.fs

Unser Modul Database kann unverändert bleiben. Im Modul Todos benötigen wir allerdings eine Funktion, um das Completed-Flag eines Eintrags zu ändern.

Auf den ersten Blick erscheint die Implementierung dieser Funktion unnötig kompliziert. Allerdings arbeiten wir bei funktionaler Programmierung mit unveränderlichen Datentypen, d. h. eine Funktion, die aus der bestehenden Liste einen Eintrag ändert, kann es nicht geben.

Stattdessen müssen wir die Liste kopieren und dabei den einen Eintrag mit dem bestehenden aus Vorlage neu erzeugen.

module Todos =

    ... // die bisherigen Funktionen

    let toggleComplete model id =
        let toggle todo =
            if id <> todo.Id then todo
            else { todo with Completed = not todo.Completed}
        model |> List.map toggle

Wir erstellen dafür eine eingebettete Funktion toggle, die ein todo auf die richtige Id prüft. Stimmt die Id, so wird ein neues todo zurückgegeben, als Kopie mit verändertem Completed Status. Im anderen Fall wird das übergebene todo unverändert zurückgegeben.

Diese Funktion verwenden wir dann in der List.map-Funktion.

Das Ergebnis ist eine neue Aufgabenliste, die wir dann an das Modul Database übergeben können. Das tun wir im router, wo alle HTTP-Requests ankommen:

let webApp =
    router {

    ... // die bisherigen Funktionen

        patchf "/api/todos/%i/toggle_complete" (fun id next ctx ->
                let model = Todos.toggleComplete (Database.getAll()) id
                Database.save model
                json (Database.getAllSorted()) next ctx
            )

Ich denke, der Code ist im Wesentlichen klar: Wir rufen die neue Funktion auf, geben das neue model an Database.save und liefern wie immer eine sortierte Liste zurück.

Neu ist der Parameter in der URL. Parallel zu den »normalen« HTTP-Verben (get, post, put, patch …), die einen festen String als URL nehmen, gibt es jeweils ein formatierte Variante (getf, postf, putf, patchf …), in deren URL-String Parameter enthalten sein können. Die Formatierungsregeln entsprechen denen bei printf oder sprintf. Alle diese Parameter werden vorne an die Parameterliste angefügt (also vor next und ctx).

Falls ihr euch fragt, warum wir den String "/api/todos/%i/toggle_complete" nicht – wie die andere URL auch – in Shared.fs abgelegt haben: Das geht leider nicht so einfach.

Sowohl sprintf als auch patchf erwarten keinen string, sondern einen typisierten Parameter, in dessen Signatur enthalten ist, welcher Variablentyp als Platzhalter erwartet wird (z. B. %i für eine Ganzzahl). Der Compiler kann das erkennen und aus dem Zeichenketten-Literal im Code den richtigen Typ ableiten. Das funktioniert aber nicht mehr, wenn die Zeichenkette einfach an eine Variable gebunden wird. In diesem Fall leitet der Compiler einfach den Typ string ab – was dann in der Verwendung bei sprintf und patchf zu einem Fehler führt.

Daher bleibt uns hier nur die Möglichkeit, diesen Formatstring direkt an dieser Stelle im Code zu hinterlegen. Was leider die Nützlichkeit des Moduls Route in Shared.fs deutlich einschränkt.

So, damit ist der Code fertig und lauffähig. Probiert es gerne einmal aus.

Zusammenfassung

In diesem – zugegebenermaßen kürzeren – Teil der Serie haben wir eine wichtige Funktionalität erstellt, nämlich eine bestehende Aufgabe verändert.

Damit kennt ihr alles Wichtige, um selbst Full-Stack-Anwendungen zu erstellen:

  • Verwendung des Model-Update-View-Pattern
  • Erstellung einer HTML-UI
  • Kommunikation zwischen Frontend und Backend mittels GET, POST und PATCH – und zwar mit und ohne Parameter in der URL
  • Parallele Verarbeitung, um eine nicht blockierende Anwendung im Frontend und Backend zu erstellen.

Die Anwendung ist hiermit feature complete.

Die letzten beiden Teile der Serie werden sich mit Sonderthemen beschäftigen: Im nächsten Teil hübschen wir das Frontend deutlich auf, der letzte Teil widmet sich dem automatisierten Testen.

Viel Spaß …

Goetz Markgraf

Goetz hat Wirtschaftsinformatik studiert und viele Jahre als Softwareentwickler und Project Manager gearbeitet. Sein Fokus liegt dabei immer auf dem Verständnis für die Situation und Herausforderungen der Kunden und darin, dies für Entwickler verständlich zu machen.
Seit 2018 ist er Consultant bei der codecentric AG.

Über 1.000 Abonnenten sind up to date!

Die neuesten Tipps, Tricks, Tools und Technologien. Jede Woche direkt in deine Inbox.

Kostenfrei anmelden und immer auf dem neuesten Stand bleiben!
(Keine Sorge, du kannst dich jederzeit abmelden.)

* Hiermit willige ich in die Erhebung und Verarbeitung der vorstehenden Daten für das Empfangen des monatlichen Newsletters der codecentric AG per E-Mail ein. Ihre Einwilligung können Sie per E-Mail an datenschutz@codecentric.de, in der Informations-E-Mail selbst per Link oder an die im Impressum genannten Kontaktdaten jederzeit widerrufen. Von der Datenschutzerklärung der codecentric AG habe ich Kenntnis genommen und bestätige dies mit Absendung des Formulars.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.