Die Anwendung mit nur einer funktionalen Programmiersprache entwickeln
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 → ein einfach zu entwickelndes REST- bzw. HTML-Backend | |
Azure → 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 → auf Babel aufbauender Transpilier nach JavaScript inkl. HTML-Erzeugung und React-Anbindung | |
ELMISH → 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 -m
bedeutet, 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 Todo
s. 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.
(Quelle: https://safe-stack.github.io/docs/component-elmish/)
Im Einzelnen:
Das Model des Frontend soll eine Liste von Todo
s 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.
Weitere Beiträge
von Goetz Markgraf
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Goetz Markgraf
Senior Agile Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.