Tutorial: F# mit SAFE-Stack – Teil 1

Keine Kommentare

Eine Anwendung mit nur einer funktionalen Programmiersprache entwickeln

Cloud Native Illustration

F# ist eine großartige Sprache. Ich habe in meiner beruflichen Laufbahn eine Menge Programmiersprachen kennengelernt, aber selten war ich so begeistert wie von F#. Das liegt vor allem an dem funktionalen Paradigma, das diese Sprache unterstützt, sowie an der extrem schlanken Syntax. Aber nicht zuletzt auch daran, dass man damit endlich wieder Full Stack eine vollständige Anwendung mit nur einer Sprache und nur einem Paradigma entwickeln kann – und das auch noch funktional!

Natürlich ist klar, dass das grundsätzlich auch mit JavaScript und Node.js funktioniert. Doch ich muss gestehen, dass ich JavaScript aus mehreren Gründen, die ich hier nicht ausbreiten möchte, nicht mag. Stattdessen halte ich den funktionalen Ansatz von F# und die (von ML und OCaml abgeleitete) Syntax für deutlich vielversprechender.

Der Vorteil des funktionalen Paradigmas

Der funktionale Programmierstil ist in vieler Munde und wird schon als möglicher nächster Paradigmenwechsel gehandelt (beispielsweise hier). Gründe dafür gibt es viele. Unter anderem unterstützt diese Art der Programmierung folgende Konstrukte besonders gut:

  • Unveränderliche Daten – kein shared state und keine side effects
  • Deklarativer Stil – Code, der zeigt, was man erreichen will und nicht, wie im Detail das erreicht wird.

Beides sind Aspekte, die sowohl im Frontend als auch im Backend als wertvoll und zielführend eingeschätzt werden. Grund genug, sich mit funktionalen Sprache zu beschäftigen, die beides primär unterstützt.

F# gehört zum .NET-Universum. Seit .NET-Core plattformunabhängig und open source ist, hat man keinen MS Windows lock-in mehr. Eine gute Einführung bekommt man auf fsharp.org. Wer noch mehr Gründe für die Sprache sucht, wird hier fündig.

Der SAFE-Stack

Um eine vollständige Anwendung zu schreiben, braucht man allerdings noch ein paar zusätzliche Bibliotheken. Diese werden als „SAFE-Stack“ bezeichnet. Jeder Buchstabe steht dabei für ein Framework, bzw. eine Technologie:

Saturn LogoSaturn → ein einfach zu entwickelndes REST- bzw. HTML-Backend
Azure LogoAzure → Integration in die Cloud-Dienste von Microsoft. Tatsächlich ist dieser „Buchstabe“ nicht besonders intensiv mit den anderen verknüpft. Ich werde ihn in diesem Tutorial nicht betrachten. Der „SAFE-Stack“ funktioniert auch mit AWS oder anderen Cloud-Anbietern – oder »on-Premise«.
Fable LogoFable → auf Babel aufbauender Transpilier nach JavaScript inkl. HTML-Erzeugung und React-Anbindung
Elmish LogoELMISH → Verwendung des Model-Update-View-Musters von Elm für F# im Frontend

Die Bibliotheken des SAFE-Stack sind sehr umfangreich und mächtig. Man findet im Netz mehrere Templates und Tutorials (z. B. hier, hier oder hier). Allerdings benutzen diese Tutorials häufig unterschiedliche zusätzliche Bibliotheken, sodass Code aus dem einen Beispiel in einem anderen nicht funktioniert und die eigentliche Struktur oftmals nicht klar wird.

Für diese Artikelserie gehe ich daher von dem minimalen SAFE-Template aus und erkläre (fast) jede Zeile. Dadurch wird der Code vielleicht etwas (wirklich nur wenig!) umfangreicher als notwendig, aber ihr werdet sehen, dass trotzdem nicht viele Zeilen F#-Code notwendig sind, um eine funktionierende Anwendung zu erstellen.

Weitere Bibliotheken könnt ihr dann gerne nach Belieben ergänzen.

Das Tutorial

In diesem und den folgenden vier Teilen werden wir eine (sehr) einfache To-do-Anwendung schreiben, also eine Anwendung, in der man Aufgaben anlegen und als erledigt markieren kann.

Dazu werden wir iterativ vorgehen. In diesem Teil geht es um das Fundament mit einer noch sehr einfachen Funktionalität. Diese wird dann in den kommenden Teilen ergänzt und verfeinert.

Ich gehe dabei davon aus, dass ihr bereits Grundkenntnisse in F# besitzt und .NET Core 3.1 (oder neuer) installiert habt. Außerdem benötigt ihr noch Node.js.

Das SAFE Template

Um unsere Anwendung zu schreiben, beginnen wir mit einem Template. Wir benutzen wie gesagt das minimale SAFE-Template, um das Beispiel einfach und verständlich zu halten.

Zunächst müssen wir das SAFE-Template erst einmal auf unserem Rechner installieren:

dotnet new -i SAFE.Template

Als nächstes legt ihr ein Projektverzeichnis an, wechselt mit cd hinein und erzeugt hier eine Instanz des Templates:

dotnet new SAFE -m

(Das -mbedeutet, dass das minimale Template genutzt wird. Lasst ihr diesen Parameter weg, so wird die Anwendung deutlich umfangreicher, aber eben auch deutlich verwirrender und komplexer.)

Der so erzeugte Verzeichnisbaum sieht auf der obersten Ebene wie eine Node.js-Anwendung aus, es gibt die üblichen Dateien und ein src-verzeichnis.

Spannend sind allerdings die drei Unterverzeichnisse von src, die ihrerseits als .NET-Projekte (mit .fsproj-Datei) angelegt sind. In dem Client-Verzeichnis befinden sich außerdem noch eine HTML-Datei und ein Unterverzeichnis für statische Web-Inhalte (wie z. B.: HTML-Seiten, Bilder oder CSS-Files).

Wenig überraschend enthält das Verzeichnis Client den Code für das Frontend und Server den für das Backend. Beide Projekte verweisen ihrerseits auf das Verzeichnis Shared, in dem Code abgelegt wird, der von beiden Teilen der Anwendung verwendet wird.

Hier zeigt sich ein Vorteil, wenn man eine einheitliche Sprache und einheitliches Projekt benutzt: Es ist möglich, Funktionen nur einmal zu entwickeln und dann in beiden Teilen der Anwendung zu verwenden, wenn das erforderlich oder hilfreich ist.

Starten

Dieses Template ist bereits funktionsfähig und kann sehr einfach gestartet werden. Dazu braucht ihr zwei Terminal-Instanzen, die beide auf das Projektverzeichnis zeigen.

Start des Servers:

cd src/Server
dotnet watch run

Damit startet der Server-Teil der Anwendung als echte .NET-Anwendung und zwar im Watch-Modus. Das bedeutet, dass die Anwendung bei jeder Änderung an einer Datei kompiliert und neu gestartet wird.

Den Client startet man wie jede Node.js-Anwendung aus dem Hauptverzeichnis des Projektes mit einem einmaligen:

npm install

und anschließend mit:

npm run start

Das Template enthält eine Webpack-Konfiguration, die einen Development-Build der Anwendung erstellt. Auch hier wird bei jeder Änderung neu kompiliert und im Browser aktualisiert.

Die bestehende Anwendung tut nichts anderes, als einen String aus dem Backend an das Frontend zu geben und dort anzuzeigen (http://localhost:8080). Wenig spannend, aber voll funktionsfähig.

Ich schlage vor, dass ihr euch die drei Dateien

  • Index.fs(Hier steckt der Code für das Frontend)
  • Server.fs
  • Shared.fs

einmal anschaut – der Code ist nicht besonders schwer zu verstehen.

Erste Iteration

Als nächstes bauen wir für die erste Iteration unsere Anwendung um. Den vollständigen Code findet ihr hier.

Shared

In dem geteilten Code bringen wir unsere Datenstruktur für die Todos unter, die sowohl im Backend als auch im Frontend verwendet wird, außerdem ändern wir den Pfad des Service, den wir anbieten möchten, und den Namen, unter dem wir ihn uns merken:

type Todo =
    {
        Id: int
        Description: string
        Completed: bool
    }

module Route =
    let todos = "/api/todos"

Server

Vor der Definition der webApp fügen wir unsere „Datenbank“ ein:

let database = [
    {
        Id = 1
        Description = "Read all todos"
        Completed = true
    }
    {
        Id = 2
        Description = "Add a new todo"
        Completed = false
    }
]

Wir simulieren eine Datenbank durch eine einfache Liste von Todos. Da wir in der ersten Iteration noch keine Veränderung an den Daten vornehmen möchten, reicht das. Wie üblich in F# brauchen wir keine Typ-Annotationen, da der Compiler durch die verwendeten Labels eindeutig den Typ Todo ableiten kann.

Dann ändern wir noch die webApp selbst:

let webApp =
    router {
        get Route.todos (fun next ctx ->
            json database next ctx)
    }

Hier wird jetzt nicht mehr ein String sondern die zuvor definierte Datenstruktur zurückgegeben. Die zusätzlichen Parameter (next und ctx) wären nicht notwendig, ich habe es mir aber angewöhnt, sie immer mitzugeben, da so die Funktionen im router einheitlicher werden. In den nächsten Teilen werden wir mindestens ctx (= Context) brauchen.

Natürlich hindert euch niemand daran, statt einem Lambda auch eine echte Funktion zu erstellen und diese an get zu übergeben.

Client (aka Index.fs)

Jetzt passen wir noch den Client an. Hier müssen wir etwas mehr umbauen, das führt aber am Ende auch nicht zu deutlich mehr Code. Wir strukturieren nur etwas um, um für spätere Erweiterungen einheitlicher zu sein, wie ihr in den nächsten Folgen sehen werdet.

Grundsätzlich arbeitet das Elmish-Framework mit einem Model, dessen initialem Zustand, verschiedenen Transformationen aufgrund von Ereignissen (Commands) sowie dem eigentlichen Erstellen der View.

Model Update View Illustration

(Quelle: https://safe-stack.github.io/docs/component-elmish/)

Im Einzelnen:

Das Model des Frontend soll eine Liste von Todos sowie eine mögliche Fehlermeldung enthalten.

type Model =
    {
        Todos: Todo list
        Error: string
    }

Wir brauchen drei anstelle von einer Message, um die Anwendung später einheitlich erweitern zu können.

type Msg =
    | Load
    | Refresh of Todo list
    | Error of exn

Dabei ist Load die Message, die anweist, die Daten asynchron vom Server zu laden. Refresh wird im Erfolgsfall und Error im Fehlerfall benutzt.

Das Model wird mit einer leeren Liste und einer Fehlermeldung gefüllt. Außerdem geben wir eine initiale Message an, und zwar die zum Laden der Todo-Liste, wie gerade eben definiert.

let init() =
    { Todos = []; Error = "" }, Cmd.ofMsg Load

In der Update-Methode unterscheiden wir unsere drei Messages.

let update msg model =
    match msg with
    | Load ->
        let loadTodos() = Fetch.get<unit, Todo list> Route.todos
        let cmd = Cmd.OfPromise.either loadTodos () Refresh Error
        model, cmd
    | Refresh todos ->
        { model with Todos = todos}, Cmd.none
    | Error err ->
        { model with Error = err.Message }, Cmd.none

Load erzeugt über Fetch.get einen Promise, der dann zum Erzeugen eines Kommandos als Container für eine Message benutzt wird. Was hier genau geschieht und was es mit diesem Cmd auf sich hat, werde ich im nächsten Teil der Serie erklären, sonst wird dieser Post zu lang.

Der restliche Code sollte klar sein: Auf Basis des aktuellen model wird ein neues, geändertes model zurückgegeben. Das Framework kümmert sich darum, den Anwendungs-State zu aktualisieren. Der letzte Schritt ist das Neuzeichnen des UI.

Das geschieht in der Funktion view:

let view model dispatch =
    div [ Style [ TextAlign TextAlignOptions.Center; Padding 40 ] ] [
        div [] [
            img [ Src "favicon.png" ]
            h1 [] [ str (sprintf "Todos: %i" model.Todos.Length) ]
            match model.Error with
            | "" -> div [] []
            | s -> p [ ] [ str s ]
            div [] ( model.Todos
                     |> List.map (fun each -> p [] [str each.Description]))
        ]
    ]

Dieser Code verwendet das Builder-Pattern, um deklarativ HTML-Elemente zu erzeugen. Jedes Elemente erhält zwei Listen als Parameter. Die erste steht für Stil- oder CSS-Attribute und etwaige Event-Listener. Die zweite enthält Kind-Elemente. Kann ein Element keine Kind-Elemente aufnehmen, so nimmt es nur eine Liste.

That’s it

Mit diesen wenigen Änderungen haben wir die erste Iteration unserer Anwendung erreicht. Jetzt zeigt die Anwendung die oben definierten Todos an. Sollte der Server nicht gestartet sein, so zeigt das Frontend eine Fehlermeldung, arbeitet aber ansonsten problemlos weiter.

Wichtig: Obwohl wir im gesamten Code keine einzige Typ-Deklaration verwendet haben, ist der Code statisch typisiert. Der Compiler nutzt sehr leistungsfähige type inference, sodass zur Entwicklungszeit Syntaxfehler leicht gefunden werden können. Außerdem benutzt eine geeignete IDE diese Informationen zur Code-Completion. Für Visual Studio Code gibt es mit Ionide das geeignete Plugin für F#.

Zusammenfassung

Wir haben mithilfe des SAFE-Templates mit wenigen Handgriffen eine Web-Anwendung mit nur einer Sprache in einem Projekt erstellt.

Der von uns geschriebene Code ist rein funktional und benutzt im Frontend und Backend dieselben sprachlichen Paradigmen und Muster. Gefühlt gibt es keine Trennung mehr zwischen Frontend und Backend – bis auf die Kommunikation über REST.

Auf dem Server verwenden wir Saturn (z. B. die Funktion router aus Server.fs), um sehr einfach einen HTTP-Service bauen zu können.

Auf dem Client unterstützt uns Elmish mit seinem model-update-view-Pattern, während wir von Fable die Übersetzung nach JavaScript und auch die HTML-Erzeugung erhalten.

Die Anwendung ist bislang noch nicht besonders komplex, aber sie ist eine gute Plattform, um in den nächsten Folgen aufbauen zu können.

Stay tuned …

Hier geht’s zum zweiten Teil der Reihe.

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.