Blog: Java, Software-Entwicklung & mehr

Elm - funktionale Programmierung im Browser

10.03.2017 Thomas Mohme

Elm - funktionale Programmierung im Browser

Was sollen wir damit? Wozu brauchen wir diesen akademischen Kram jetzt auch im Browser?
Ein Gedanke, den sicherlich viele haben, wenn sie von funktionaler Programmierung hören.

Zum ersten Mal bin ich durch die Präsentation "Let's Be Mainstream" von Evan Czaplicki (dem Schöpfer von Elm) auf der Curry-On 2015 auf Elm aufmerksam geworden.
In dem sehenswerten Video präsentiert Evan Czaplicki das Ziel von Elm: Eine Programmiersprache mit zugleich sehr guter Usability und sehr guter Wartbarkeit zu schaffen.

So richtig Lust auf's Ausprobieren hatte ich dann nach Richard Feldmans Vortrag mit dem leicht provokanten Titel "Making the Back-End Team Jealous: Elm in Production". Er berichtet dabei sehr bildhaft und unterhaltsam von seinen Erfahrungen mit Elm im Produktivbetrieb.

Nach diversen weiteren Appetithäppchen habe ich mir nun endlich die Zeit genommen und Elm selber einmal ausprobiert.
Als halbwegs anspruchsvolle Aufgabenstellung habe ich mir vorgenommen, das Front-End der Spring PetClinic in Elm zu implementieren.

Bevor ich meine Erlebnisse schildere möchte ich Elm kurz vorstellen:

Was genau ist Elm eigentlich?

Elm ist eine noch recht junge Sprache (* 2012), die nach JavaScript kompiliert wird und für den Einsatz im Browser gedacht ist.
Die Mittel für die eigentliche Interaktion mit dem Browser sind nicht Bestandteil der Sprache, sondern als Funktionen der Core-Library realisiert.
Während sich JavaScript im Tiobe-Index aktuell auf Platz acht befindet, ist Elm dort nur im Bereich von 51-100 angegeben.
Die in Elm verwirklichten Konzepte beeinflussen nicht nur die primäre Elm-Community: Im Umfeld von React sind Redux und Cycle.js Versuche, die Elm-Architektur direkt mit JavaScript zu verwenden.
Die Elm Runtime verwendet - wie auch React - ein "virtual DOM".
Ich beziehe mich im folgenden auf die aktuelle Version 0.18.

Elm unterscheidet sich deutlich von anderen Sprachen, die die Mängel von JavaScript beheben wollen: Statt "kleinere" Modifikationen vorzunehmen (TypeScript, CoffeeScript) ist Elm eine vollständig andere, rein funktionale Sprache, die für die Erstellung von Front-Ends optimiert ist.

JavaScript:


function add(x, y) {
    return x + y;
}

Elm:


add: number -> number -> number
add: x y = x + y

Die Elm Syntax stammt nicht von C ab, sondern vom Zweig der ML-Sprachen (Haskell, OCaml, F#) und ist damit für jemanden mit JavaScript Hintergrund erstmal gewöhnungsbedürftig.
Optisch fällt zuerst die fast vollständige Abwesenheit von von Klammern und Kommas auf. Warum brauchen wir die vielen Klammern & Co. eigentlich in JavaScript/Java?
Inhaltlich geht es weiter:

  • Elm ist statisch typisiert, ohne Typdeklarationen zu erfordern.
    Die obere Zeile im Elm-Beispiel (im Elm-Jargon "Typ-Annotation" genannt) ist optional, aber best-practice für Dokumentationszwecke.
  • Alle Daten in Elm sind immutable.
  • Elm erlaubt keine Seiteneffekte. Kein jQuery, kein AJAX, nicht einmal console.log sind ohne weiteres möglich.
    Der Compiler wacht darüber, dass alle Funktionen in Elm 'pur' sind.
  • Elm nutzt "static, inferred, structural typing", d.h.
    • eine Expression hat immer den gleichen Type (static)
    • Typen müssen nicht angegeben werden, sondern werden vom Compiler ermittelt (inferred)
    • Typen müssen nicht formal kompatibel definiert werden, sondern sind automatisch zueinander "passend", wenn sie eine "passende" Struktur haben (structural)
  • Es gibt weder null noch undefined.
  • Es gibt kein try ... catch.

Insgesamt finde ich die Sprache (trotz meines Java-Hintergrundes) einfach zu erlernen.

Wenn man sich auf all dies einlässt bekommt man dafür eine Umgebung, in der es praktisch keine Runtime Exceptions gibt.
Das Schlimmste, was mir während meines experimentierens passiert ist, war eine Endlos-Schleife.

Hello, World!

Um mit Elm zu arbeiten benötigt man die Elm Platform (Compiler, etc.), die man von der Elm Website herunterladen kann und die sich einfach OS-spezifisch installieren lässt. Alternativ lässt sich Elm auch über npm installieren - wir bewegen uns in der JavaScript Welt.

Für Elm Programme gilt die Empfehlung, immer möglichst einfach anzufangen und bei Bedarf später zu Refaktorisieren.
Für meine Experimente hat dieses Vorgehen gut funktioniert und ich kann mir vorstellen, dass es auch in größeren Projekten gut funktioniert.
Die entscheidende Hilfe hierfür ist das Typsystem, durch das maschinelle Unterstützung erleichtert wird und der Compiler, der es praktisch unmöglich macht, während eines Umbaus Fehler entstehen zu lassen, die erst zur Laufzeit bemerkt werden.

Ich habe hiermit begonnen:


import Html exposing (text)

main =
  text "Hello, World!"

Im Gegensatz zu JavaScript haben Elm Programme genau einen Einstiegspunkt: Die Funktion mit dem Namen main.
Elm kennt verschiedene zulässige Typen für die main Funktion. Im Hello-World-Beispiel ist es die einfachste Variante mit der Typ-Annotation:


main: Html

Das heißt, die Funktion main hat keine Argumente und liefert Html zurück.

Dies übersetzt man mit elm make <Dateiname> zu einer ~180kB großen index.html Datei, die man im Browser anschauen kann.
In der index.html ist alles enthalten, was Elm zur Laufzeit braucht.
Das in den 180kB enthaltene generierte JavaScript ist in keiner Weise komprimiert, sondern eher für Menschen lesbar.

Der nächste Schritt

Nach dieser ersten Hürde habe ich mich an ein erstes Programm mit Benutzer-Interaktion aus dem Examples-Fundus "gewagt".
Ein Zähler, den man per Button inkrementieren und Dekrementieren kann:


-- Read more about this program in the official Elm guide:
-- <a href="https://guide.elm-lang.org/architecture/user_input/buttons.html" target="_blank" rel="nofollow">https://guide.elm-lang.org/architecture/user_input/buttons.html</a>

import Html exposing (beginnerProgram, div, button, text)
import Html.Events exposing (onClick)

main =
  beginnerProgram { model = 0, view = view, update = update }

view model =
  div []
    [ button [ onClick Decrement ] [ text "-" ]
    , div [] [ text (toString model) ]
    , button [ onClick Increment ] [ text "+" ]
    ]

type Msg = Increment | Decrement

update msg model =
  case msg of
    Increment ->
      model + 1
    Decrement ->
      model - 1

Im Browser erscheint ein --Button, darunter der Wert "0", darunter ein +-Button.

Mit diesem unscheinbaren Programm sind wir schon mitten drin in der "Elm-Architecture", die das grundsätzliche Ablaufmodell in Elm beschreibt.
Fangen wir oben an:

  1. Mit -- kann man Kommentare bis zum Zeilenende erstellen.
  2. Elm ist modular aufgebaut und fördert die Modularisierung der eigenen Programme. Mit den import Statements werden - ähnlich wie in Java - Funktionen aus externen Modulen lokal verfügbar gemacht.
  3. Ein alter Bekannter, die main Funktion. Diesmal in der nächsten Ausbaustufe:
    • Es wird die Funktion beginnerProgram aus dem Modul Html aufgerufen und dabei ein Record (entspricht ungefähr einem JavaScript Objekt) mit den Feldern model, view und update übergeben.
    • model ist (wie in der ursprünglichen MVC-Architektur) der Zustand des Programms. In diesem Fall ist es nur ein einziger Integer-Wert. In "echten" Programmen ist dies eher eine komplexe Datenstruktur.
    • Als view wird die weiter unten stehende Funktion view übergeben. Funktionen sind "First Class Citizens" in Elm und "higher-order functions" sind in Elm überall anzutreffen.
      Hiermit teilen wir der Elm-Runtime mit, dass für die Html-Erzeugung unsere view Funktion aufgerufen werden soll.
    • In update wird die weiter unten stehende update Funktion übergeben.
      Hiermit teilen wir der Elm-Runtime mit, dass bei eintreffenden Nachrichten unsere update Funktion aufgerufen werden soll.
  4. Mit type Msg = Increment | Decrement wird der Union-Typ Msg erzeugt. Er besteht aus den beiden Werten Increment und Decrement. Hier übernimmt Msg die Aufgabe eines Java-Enums.

Was passiert nun zur Laufzeit?
Wenn die index.html geladen wird startet der Browser das darin enthaltenen Script und damit unsere main Funktion.
Durch den Aufruf von beginnerProgram haben wir die Elm-Runtime instruiert, was im folgenden zu tun ist:

  1. Die Elm-Runtime ruft unsere view Funktion auf, die für die Html-Erzeugung zuständig ist. Unsere view Funktion ruft in dem Beispiel hierzu die Funktionen div und und button aus dem Html Modul auf, die jeweils entsprechende Html-Elemente erzeugen.
  2. Wenn man auf einen der Buttons klickt, dann wird eine Msg erzeugt (konkret: Decrement bzw. Increment) und unsere update Funktion mit dieser Nachricht und unserem "model" als Argumenten aufgerufen.
    Als Ergebnis liefert die update Funktion ein neues Model (alles ist immutable).
  3. Anschließend ruft die Elm-Runtime unsere view Funktion mit dem neuen Modell als Argument auf, um eine aktualisierte Ansicht zu erzeugen.

Die Elm-Runtime sorgt dafür, dass nicht wirklich jedes mal neues Html erzeugt wird, sondern nur die Änderungen gegenüber der vorherigen Version in den DOM-Tree eingearbeitet werden. Dies ist für Entwickler jedoch vollständig transparent und eine große Entlastung.

Für Interaktionen mit der Außenwelt wird dieses Ablaufmodell noch um "Commands" und "Subscriptions" ergänzt. Grob vereinfacht fügen sie sich jedoch ähnlich ein, wie unsere Msg Nachrichten oben.

Diese drei sauber voneinander getrennten Elemente - Model, Update-Funktion und View-Funktion - finden sich in jedem Elm-Programm und bilden die sogenannte Elm-Architektur.

Strukturell ist das dann auch schon das grundlegende Wissen, das man braucht, um Elm-Programme zu schreiben.
Es gibt keine "Magie", die viele Dinge automatisch macht (oder auch nicht).
Alles ist explizit und - wie ich finde - gut nachvollziehbar. Diese Einfachheit und Klarheit empfinde ich als eine große Stärke von Elm.

Meine Erlebnisse mit Elm

Nachdem ich hoffentlich einen ersten Eindruck vermittelt habe, wie Elm-Programme aussehen und strukturiert sind, möchte ich nun mehr auf meine subjektiven Erfahrungen bei meinem Experiment eingehen.

Einfach zu erlernen

Wenn man sich näher mit Elm beschäftigt fällt zunächst auf, wie "klein" der Sprachumfang ist:
Es gibt "die üblichen" primitiven Datentypen Boolean, Char, String, Int und Float.
Es gibt einfache Literale, Tupel, Records und Listen. Für Conditionals haben wir if-Expressions und case-Expressions.
Es gibt Funktionsaufrufe, Infix-Operatoren (die auch nur Funktionsaufrufe mit "Syntactic-Sugar" sind) und let-Expressions als einzige Möglichkeit, immutable "Variablen" zu definieren.

Insbesondere gibt es kein "null", keine "Exceptions" und keine weiteren Kontrollstrukturen wie Schleifen.
Nicht einmal Statements (die im Gegensatz zu Expressions keinen Wert produzieren) gibt es.

Das Typ-System dürfte für viele die größte Herausforderung beim erlernen sein. Es hat Ähnlichkeit mit dem von Scala, ist aber wesentlich einfacher, da Elm keinerlei Objektorientierung unterstützt und somit die ganzen, aus der OO-Vererbung resultierenden Probleme einfach nicht vorhanden sind.

Als funktionale Sprache unterstützt Elm natürlich auch "partial application" und "Currying", was auf Wikipedia sehr mathematisch beschrieben ist, aber in der Elm-Praxis wunderbar einfach und problemlos funktioniert - auch wenn man sich nicht tiefer mit der Theorie dahinter beschäftigt hat.
Auch Pattern-Matching ist natürlich möglich. In der gesamten Elm-Dokumentation wird bewusst auf den ganzen mathematischen Hintergrund verzichtet. Man bekommt einfach die Informationen, die man braucht, um Elm anzuwenden.

Natürlich erfordert Elm ein gewisses Umdenken bei der Strukturierung von Aufgaben wenn man andere Ansätze (z.B. Objektorientierung) gewohnt ist. Dies liegt jedoch eher an den eigenen festgefahrenen Denkmustern, als an Elm.

Weil Elm viel weniger Klammern und Kommas erfordert, sieht Elm Code erstmal "komisch" aus, wenn man Java gewohnt ist.
Nachdem ich mich an die Elm-Optik gewöhnt habe frage ich mich eher, warum diese ganzen Trennzeichen in anderen Sprachen erforderlich sind.

Ich finde es jedenfalls sehr positiv, wenn ich statt (in Java)


private static enum Demo {
    Alpha,
    Beta
}
in Elm einfach

type Demo = Alpha | Beta
schreiben kann.

Insgesamt war ich positiv überrascht, wie einfach die Sprache erlernbar ist.
Einen wichtigen Teil tragen natürlich die gute Dokumentation und die vielen im Netz auffindbaren Beispiele dazu bei.

Die Anwendung

In den anfänglichen Beispielen in diesem Artikel habe ich Elm einfach eine index.html Datei erzeugen lassen.
Das ist zwar für die ersten Schritte ganz bequem, aber nicht das, was man in einer "echten" Anwendung braucht.

Mit Elm ist es aber auch problemlos möglich die Elm-Anwendung in eine bestehende Webseite zu integrieren.
Hierzu habe ich mir eine einfach index.html selber geschrieben, die über den gleichen Header wie das Original aus der Spring-PetClinic verfügt und mein Elm-Programm im "Fullscreen" Modus einbindet:


<!DOCTYPE HTML>
<html>
<head>
    ...
    <link rel="shortcut icon" type="image/x-icon" href="/resources/images/favicon.png">
    <link rel="stylesheet" href="/resources/css/petclinic.css"/>

    <script type="text/javascript" src="/elm/elm.js"></script>

    </head>

    <body>
    </body>

<script type="text/javascript">
  var app = Elm.Main.fullscreen();
</script>
</html>

Wie man in der index.html sehen kann, referenziere ich die Original petclinic.css - eine einfache Integration von Teilen der alten (SpringBoot/Thymeleaf) und der neuen Elm-Anwendung ist problemlos möglich.

Hierzu lässt man elm make statt der index.html einfach eine JavaScript Datei (oben: elm.js) erzeugen.
Alternativ ist es auch möglich, ein Elm-Programm in einem Teil der Webseite laufen zu lassen.

Als Entwicklungsumgebung habe ich IntelliJ IDEA mit dem Elm-Plugin genutzt.
Es bietet bereits eine erstaunlich gute Unterstützung. Neben dem obligatorischen Syntax-Highlighting werden auch Auto-Completion (teilweise), einfache Refactorings und die Navigation zur Definition von Symbolen (auch zu externen Modulen) unterstützt.

Der Compiler fällt durch gut verständliche und hilfreiche Fehlermeldungen auf:


-- NAMING ERROR ------------------------------------------ src/main/elm/Main.elm

Cannot find type Location

83| parse : Parser -> Location -> Maybe NavMsg
                      ^^^^^^^^
Maybe you want one of the following?

Navigation.Location


-- TYPE MISMATCH --------------------------------------- ./src/main/elm/Vets.elm

The 2nd and 3rd branches of this case produce different types of values.

42|     case message of
43|         ViewAsJson -> ( model, Cmd.none )
44|         ViewAsXml -> ( model, Cmd.none )
45|>        Loaded (Err error) -> model
46|         Loaded (Ok loadedVets) -> ( Model loadedVets, Cmd.none )

The 2nd branch has this type:

( Model, Cmd msg )

But the 3rd is:

Model

Hint: All branches in a case must have the same type. So no matter which one
we take, we always get back the same type of value.

Gerade Anfänger profitieren hiervon stark.
Teilweise habe ich mir bei Refactorings nicht die Mühe gemacht, alle betroffenen Stellen selber vorab zu identifizieren und zu ändern. Stattdessen habe ich mich durch die Fehlermeldungen des Compilers leiten lassen, was gut funktionierte.

Wie allgemein empfohlen habe ich bei meinem PetClinic Experiment sehr einfach angefangen: Der gesamte Code befand sich in einer Datei.
Solange ich nur mit der Welcome-Page zu tun hatte, hat das noch gut funktioniert.
Als ich dann die nächste Seite ("Veterinarians") in Angriff nahm, stand der erste grundsätzliche Umbau an: Im Gegensatz zur "Welcome"-Seite ist der Inhalt dieser Seite nicht einfach statisch, sondern wird durch einen Rest-Service der Spring-Applikation bereitgestellt. Daher sollte diese Seite in ein eigenes Modul wandern.

Nach ein wenig Suchen bin ich bei elm-tutorial.org mit einem passenden Beispiel fündig geworden.
Der Umbau nach diesem Schema hat gut funktioniert und ich glaube, dass das Grundkonzept geeignet ist, damit auch größere Anwendungen zu strukturieren.

Nachdem ich für die "Veterinarians" Seite ein eigenes Modul hatte, habe ich mich daran gemacht, die Daten per Http-Request zu besorgen.
Das geht in der streng funktionalen Elm Welt aber nicht direkt - schließlich wäre das eine Seitenwirkung, die den Funktionen nicht erlaubt ist.
Stattdessen muss man als Ergebnis der update Funktion ein "Command" erzeugen, dass beschreibt, was die Elm-Runtime ausführen soll: In diesem Fall einen Http-Request.
Auch hierfür fand sich im offiziellen Elm-Guide "An Introduction to Elm" sehr schnell ein gut nachvollziehbares Beispiel.

Zunächst hatte ich an dieser Stelle trotzdem etwas zu kämpfen, da ich immer nur einen "NetworkError" von Elm zurückbekommen habe, obwohl ich mit den Browser Debug-Tools sehen konnte, dass der Request erfolgreich ausgeführt wurde.

Ich bin einfach in die CORS-Falle getappt. JavaScript-Experten werden darüber wahrscheinlich nur müde lächeln.
Um den Build-Prozess der ursprünglichen Spring PetClinic nicht modifizieren zu müssen, habe ich meine index.html Datei direkt im Browser geöffnet, anstatt sie durch die SpringBoot Applikation ausliefern zu lassen.
Das führte dazu, dass der Http-Request von der Elm "Veterinarians" Seite ohne eigene Domain an die SpringBoot PetClinic gesendet wurde. Die SpringBoot PetClinic (Domain "localhost") antwortet darauf mit einer Response die den Zugriff von anderen Domains aus nicht explizit erlaubt, woraufhin der Browser an Javascript (=> die Elm-Runtime) zurückmeldet, dass es einen "NetworkError" gegeben hat. Lange Rede, kurzer Sinn: Nachdem ich verstanden hatte was eigentlich passierte, war der Workaround einfach. Ich habe die SpringBoot PetClinic angewiesen, Cross-Origin Requests zu erlauben.

Das CORS Problem (das im Grunde nichts mit Elm zu tun hat) war während des gesamten Experiments das einzige, dass nicht innerhalb weniger Minuten gelöst war.

Nachdem auch die "Veterinarians" Seite funktionierte, habe ich mich daran gemacht, die in den ersten Schritten ausgeblendete Navigation wiederherzustellen.
Anfangs habe ich einfach die Links auf den ursprünglichen Seiten durch Buttons ersetzt, was geholfen hat, die Elm-Anwendung schnell zum laufen zu bekommen. Bei einer "richtigen" Anwendung im Browser sollte aber auch die Browser-eigene Navigation (Seite zurück und vor) funktionieren und die im Browser angezeigte Url passen.
Bei dem simplen Elm-Programm, mit dem ich gestartet bin, war das noch nicht der Fall.

Ein weiteres mal ließen sich die benötigten Werkzeuge zusammen mit einem passenden Tutorial schnell finden.
Hiefür habe ich mich als erstes davon verabschiedet, die index.html als Datei in den Browser zu laden. Statt dessen wird sie nun als "static asset" von der SpringBoot PetClinic ausgeliefert. Auf eine vollständige Automatisierung des Build-Prozesses habe ich vorerst verzichtet, weil dies nicht der Bereich ist, über den ich bei diesem Experiment etwas lernen will.

Auch dieser Umbau funktionierte ohne wirkliche Probleme.
Allerdings muss an dieser Stelle drauf hingewiesen werden, dass das nicht einmal sechs Monate alte Tutorial bereits veraltet war: Es basiert noch auf Elm 0.17, während ich mit Elm 0.18 arbeitete.

Die erforderlichen Anpassungen waren zwar mit wenig Aufwand durchführbar, aber es ist ein generelles Problem im Elm-Universum, dass es sich schnell weiterentwickelt. Bei allem, was man im Web findet, muss man zuächst prüfen, ob es noch aktuell ist.

In einem weiteren Schritt habe ich die Suche nach Besitzern und die Anzeige der Besitzer-Liste umgestellt, wofür eine neuer JSON-Rest-Service in der SpringBoot PetClinic erforderlich wurde.
Auch dies ließ sich reibungslos in einem weiteren Elm-Modul realisieren.

Und sonst noch . . .

Elm-Module unterliegen dem "Semantic Versioning".

Für alle externen Module, die man einbindet, wird auch die gewünschte Version angegeben - wie es auch in JavaScript oder mit Maven/Gradle/etc. gemacht wird. Es ist aber in keiner Weise sichergestellt, dass man inkompatible Änderungen zwischen Versionen "von außen" an der Versionsnummer erkennen kann.

Anders, als in diesen anderen Umgebungen, wird "Semantic Versioning" in Elm jedoch durch das elm-package Tool beim publizieren durchgesetzt.
Hierdurch soll es praktikabel werden, bei Abhängigkeiten Versions-Ranges anzugeben.
Vermutlich verbessert sich dadurch die Stabilität bei der Nutzung von Versions-Ranges. Allerding kann elm-package nur die Schnittstellendefinitionen betrachten. Wenn das dahinter liegende Verhalten geändert wird, ist man trotzdem aufgeschmissen.

Mein Beispielprojekt lebt jedenfalls noch nicht lange genug, um hier eine Einschätzung des praktischen Nutzens vornehmen zu können.

Was ist hängen geblieben?

Insgesamt lässt sich feststellen, dass die Elm-Architektur mit ihrer strikten Trennung von Model, View- und Update-Funktion einen gut strukturierten und skalierbaren Eindruck macht.
Ich finde, die Elm-Architektur ist näher an dem historischen Urahn Smalltalk-MVC, als so manches, was heutzutage "MVC" im Namen trägt.

Bei meinem noch nicht allzu großen Experiment waren die durch Elm bereitgestellten Mittel zur Modularisierung vollkommen ausreichend.
Wie sich dies in wirklich großen Projekten mit tief verschachtelten Komponenten entwickelt, wird sich zeigen müssen.

Die strikten Prüfungen des Compilers haben mich bei Refactorings (aber auch sonst) davor bewahrt, notwendige Änderungen zu vergessen.
Bei JavaScript hätte dies zu langwierigen Debug-Sessions oder späteren Laufzeitfehlern geführt. Fast alles, was ich geschrieben habe lief auf Anhieb, nachdem es durch den Compiler ging. Eine Ausnahme war das Parsen der JSON-Antwort vom Server. Aber selbst hier erhält man eine Hamcrest-artige Fehlermeldung á la "Erwartet habe ich ..., aber bekommen habe ich ...".
Mit keinem einzigen Umbau habe ich etwas an der Elm-Anwendung kaputt bekommen.
Das spart viel Zeit und man kann sich mit Tests wirklich auf fachliche Logik konzentrieren.

Im Gegensatz zu einer Java Umgebung verweigert der Compiler die Übersetzung, wenn man zirkuläre Abhängigkeiten zwischen den Modulen hat. Außerdem sind transitive Abhängigkeiten nicht automatisch verfügbar.
Was zunächst nach einer unnötigen Einschränkung aussieht ist aus meiner Sicht sehr sinnvoll: Zirkuläre Abhängigkeiten sind häufig ein Hinweis darauf, dass gerade ein "Big Ball of Mud" entsteht. Und die Möglichkeit, transitive Abhängigkeiten nutzen zu können, ohne sie deklariert zu haben, verschleiert die reale Komplexität.

Es gibt für Elm noch keinen Debugger, mit dem man sich step-by-step durch das Programm hangeln kann.
Ich bin in meinem überschaubaren Experiment auch ohne diese Unterstützung ausgekommen. Ab und zu mal eine Ausgabe in die Developer-Console hat mir gereicht.
Allerdings ist seit der aktuellen Version 0.18 ein einfacher "time travelling debugger" (der allerdings noch mit Kinderkrankheiten zu kämpfen hat) Bestandteil der Elm Plattform.
Das Konzept dahinter ist so einfach wie revolutionär: Da sowohl jegliche Interaktion durch die Elm-Runtime als Argument in die Update-Funktion hineingegeben wird, als auch jegliche Zustandsänderung aus dem Ergebnis der Update-Funktion entnommen werden kann, braucht nur beides aufgezeichnet zu werden, um den Zustand der eigenen Anwendung zu jedem Zeitpunkt nachvollziehen zu können.
Auf diese Weise ermöglicht es der Debugger, jeden einzelnen Zustand seit dem Start der Applikation nachträglich zu betrachten.

Für Unit-Tests muss man leider die so schön einfache und deterministische, reine Elm-Welt verlassen, denn zur Ausführung der Tests wird Node.js benötigt.
Die gute Seite ist, dass sich alle Funktionen einfach testen lassen, da sie ja keine Seitenwirkungen haben: Sie liefern ein Ergebnis zurück, dass einzig von den hereingegebenen Werten abhängt. Good Bye Mock Libraries.

Das sehr leichtgewichtige Anlegen neuer Typen als one-liner ermuntert dazu, viel mit Domain-Typen (z.B. "UserId") statt mit primitiven Datentypen (z.B. "Int") zu arbeiten.
Dies führt wiederum zu verständlicherem Code. Aber auch darüber hinaus macht Elm es einfach, durch fachlich passende Typisierung Fehler zu vermeiden und gleichzeitig die fachliche Ausdruckskraft und damit die Verständlichkeit des Cdes zu verbessern: Richard Feldman zeigt in "Making Impossible States Impossible" auf beeindruckende Art, wie es geht.
Schon allein die Beschäftigung mit Elm öffnet den Blick in dieser Richtung - das Vorgehen an sich ist auch in anderen Sprachen anwendbar.

Allgemein fand ich den Umgang mit Elms "Union-Types" (auch "Algebraische Daten Typen" genannt), nach einer kurzen Eingewöhnung sehr positiv.
Anfangs dachte ich: "So einfach? Da muss ich was übersehen haben..."
Aber es ist wirklich so einfach ;)

Auch wenn ich noch keine wirklichen Schattenseiten von Elm kennengelernt habe, gibt es doch ein paar problematische Dinge:

  • Elm ändert sich sehr schnell. Bei einem länger lebenden Projekt muss man sich darauf einstellen, Versions-Upgrades durchzuführen.
  • Elm hat zwar eine sehr schnell wachsende Community, ist aber zur Zeit noch stark von seinem Schöpfer Evan Czaplicki abhängig.
  • Eine schrittweise Migration einer bestehende SPA zu Elm dürfte zwar möglich, aber trotzdem schwierig sein.
  • Die Integration von JS-Komponenten, die das DOM direkt manipulieren, in eine Elm-Anwendung (die auf einem virtual DOM basiert) ist schwierig. Dies gilt genauso für andere virtual DOM basierte Frameworks wie z.B. React. Richard Feldman zeigt hier einen Ausweg mit Web Components auf.
  • Die Darstellungsprobleme in dem neuen Debugger.
  • Die Tools sind imho noch nicht universell genug.
    Ich habe es geschafft, den Compiler (wahrscheinlich bei der Typ-Inferenz) durcheinander zu bringen.

Trotz dieser Probleme glaube ich, dass der Einsatz von Elm in einem kleineren, nicht-kritischen Projekt schon jetzt sehr gewinnbringend sein kann.

Es gibt nur wenige Umgebungen, in denen man so schnell Erfolge erzielt und die geschaffenen Dinge trotzdem von Anfang an verständlich sind und es dauerhaft bleiben.
Mir hat die Beschäftigung mit Elm jedenfalls viel Spaß gemacht.

Quellen

Eine Liste empfehlenswerter Seiten zu Elm: