- F# wird zunächst mit dem fable Compiler in einen Syntaxbaum konvertiert.
- diese Datenstruktur wird mit babel dann zu JavaScript umgewandelt.
- wahlweise werden zahlreiche JavaScript Dateien mittels webpack zu einer Datei, was den Ladevorgang beschleunigt und vereinfacht.
Warum will man das tun?
Abgesehen davon, dass ich damit endlich einen Grund habe, mich mehr mit F# zu befassen, kann es durchaus Vorteile haben, in F# zu programmieren und die Starrheit bzw. den präzisen Umgang von F# mit Datentypen zu genießen. Gegenüber JavaScript bietet F# nämlich mindestens diese Vorteile:
- F# ist stark typisiert. Arithmetische Operationen wie "5" + 2 compilieren gar nicht erst anstelle unerklärbares Verhalten wie in JavaScript zu präsentieren.
- F# bietet algrabraische Datentypen: Bei einem Record müssen alle Felder gefüllt sein und eine discriminated Union benötigt pro Alternative nur die dafür definierten Werte, diese aber vollständig.
- Der Umgang mit "null" oder "undefined" ist kein Thema für einen F# Entwickler, dafür nutzt man die sogenannten Options-Typen, die das Fehlen eines Wertes explizit modellieren. Werden diese Typen eingesetzt, ist eine Behandlung eines fehlenden Wertes zwingend notwendig damit das Programm compiliert.
- Der Compiler bietet zwar Typ-Inferenz (der Datentyp von Parametern wird typischerweise nicht angegeben sondern aufgrund der Verwendung erkannt), besteht aber darauf, dass eine Funktion immer nur in der vorgesehenen Weise verwendet wird.
- Fallunterscheidungen müssen vollständig sein – fehlende Behandlung von Alternativen führen zu Compiler-Fehlern.
- Viele Datentypen sind unveränderlich (immutable) und garantieren damit, dass jeder Empfänger auch die gleichen Daten erhält.
Gibt es in F# programmierte Frameworks?
Selbstverständlich – und was für welche :-) Inspiriert von der Programmiersprache Elm gibt es mehrere Implementierungen der Elm Architektur für F#. Eine davon ist fable-elmish, der ich mich für den Rest dieses Artikels widmen möchte.Berühmt geworden ist die Elm-Architektur durch ihre Einfachkeit. Eine damit programmierte Single-Page (oder auch iOS- bzw. Android-native) Applikation wird in typischerweise kleine Komponenten aufgeteilt, die durch geeignete Komposition dann zu einer zunehmend größeren Anwendung wachsen. Das ist bei funktionaler Programmierung ja nichts neues, für Leute, die eher an klassische JavaScript Frameworks denken erfordert es etwas Umdenken.
Jede Komponente besitzt hierbei
- Einen Datentyp [Msg] für alle Nachrichten, die von oder zu dieser Komponente gesandt werden. Das ist meist eine discriminated Union mit allen notwendigen Alternativen nebst zusätzlichen Transport-Daten.
- Ein weiterer Datentyp [Model] stellt die möglichen Zustände dar. Von einem leeren Datentyp für zustandslose Komponenten über einfache Typen wie boolsche Werte oder Zahlen bis hin zu Records ist hier alles denkbar. Was auch immer notwendig ist, den internen Zustand der Komponente zu speichern wird hier deklariert.
- Eine Initialisierungs-Funktion [init] (wahlweise mit oder ohne Parameter) erzeugt eine basierend auf dem Parameter definierte initiale Befüllung für das Model sowie (falls notwendig) nach der Initialisierung abgesandter Nachrichten.
- Eine Aktualisierungs-Funktion [update] erhält eine Nachricht und den bisherigen Zustand des Models und liefert einen neuen Wert für das Model sowie im Anschluß zu versendende Nachrichten.
- Eine Anzeige-Funktion [view] erhält ein Model und liefert eine optische Repräsentation des Models. Das kann via VirtualDom Implementierungen oder durch den Einsatz von React passieren. Die Folge davon ist die Aktualisierung der Anzeige im Browser.
Besonders interessant an diesem Ansatz ist, dass jede Komponente ausschließlich für sich selbst sorgt und bestenfalls zusätzliche Dinge an Kind-Komponenten delegiert. In meinen Beispielen werden sämtliche Nachrichten und Modelle in der jeweiligen Komponente gespeichert, ein zentrales F#-Modul dafür wäre genau so gut denkbar und findet sich in zahlreichen Beispielen.
Hinter den Kulissen arbeitet im Kern der Fable-Elmish Anwendung ein F# MailboxProcessor (die F# Standard-Bibliothek Variante einer Actor-Model Implementierung) sämtliche auftretenden Nachrichten ab und koordiniert die notwendigen update- und view-Aufrufe.
Die Vorbereitung ist ein steiniger Weg
Um ein neues Projekt zu beginnen, sind eine Reihe von Dingen notwendig.- Anlegen eines F# Projektes (optimalerweise mit .fsproj Datei)
- Erzeugung eines F# Moduls für die Haupt-Komponente (weitere dann später)
- Erstellung einer package.json Datei und Installation diverser node.js Module
- Um Tipparbeit zu sparen Hinterlegung zweier Konfigurations-Dateien für den fable-Compiler (fableconfig.json) und das Web-Pack (webpack.config.js). Letzteres ist sinnvoll, damit als Ergebnis des Compilierungs-Schrittes nur eine JavaScript Datei entsteht.
- Bearbeitung einer index.html Datei über die das JavaScript in den Browser gelangt
Ein Anfang mit all diesen Dateien vorbereitet ist hier als Repository vorhanden.
Danach kann erstmals der Compiler angeworfen werden und unsere einfache Applikation kann gestartet werden. Selbstverständlich lässt sich die erzeugte JavaScript Datei auch mit anderen Web-Servern als denen von node.js (ASP.NET MVC oder Web API zum Beispiel) ausliefern. Spätestens wenn man JSON Daten via Web API laden möchte wird das erforderlich sein.
Eine zusätzliche Komponente aufnehmen
Gilt es eine bestehende Applikation zu erweitern, benötigen wir lediglich ein weiteres F# Modul für die weitere Komponente.
- die .fsproj Datei muss um den Pfad zu dieser Datei erweitert werden. Hierbei an die Compile-Reihenfolge denken und diese Datei vor der Haupt-Applikation compilieren. Visual Studio übernimmt das automatisch, andere Editoren erfordern diesen Schritt.
- die Haupt-Applikation greift auf die neue Komponente zu und muss in ihren Funktionen init, update und view an entsprechenden Stellen auf die gleichnamigen Funktionen der Komponente delegieren.
- Das Model der Haupt-Applikation muss ebenfalls den zustand der neuen Komponente mit speichern und dementsprechend erweitert werden.
Die Änderungen, die dafür notwendig sind, habe ich für eine relativ einfache Komponente im "add_component" Branch eingecheckt. Um es nicht zu einfach zu machen hat die Komponente zumindest ein simples Model sowie ein einfaches Verhalten, welches einen Ladevorgang (dieser müsste allerdings noch implementiert werden) simuliert.
War's das?
Keineswegs. Wir haben in diesem Artikel lediglich an der Oberfläche gekratzt. Dadurch dass die einzelnen Lebenszyklen und Verhaltensweisen auf puren Funktionsaufrufen basieren, sind sehr viele Erweiterungen denkbar. Für einige gibt es bereits fertige Module, die man in seine Programme einbinden kann. Zum Beispiel:- Aktualisieren der Browsernavigation beim Wechsel einer Seite. Umgekehrt führt eine eingegebene URL zum Ansteuern der gleichen Seite
- Nachrichten können auch außerhalb der Komponenten erzeugt werden (z.B. Timer oder Websocket-Nachrichten) und gezielt an einzelne Komponenten weiter gegeben werden.
- Nachrichten lassen sich mitlesen, zum Loggen zum Beispiel
Ich hoffe ich habe einigen von euch Appetit auf Fable gemacht und möchte allen Leuten, die hinter dem Fable-Projekt stehen einmal meinen Respekt und Dank aussprechen. Ich persönlich würde mich über Veröffentlichungen zu Erweiterungen, Tipps, Tricks und Anregungen rund um Fable sehr freuen!