A New Hope – Zuverlässige Frontends mit Elm

Max Burri

Schon seit einiger Zeit beschäftigen sich die beiden Puzzlers Andreas Maierhofer und Max Burri mit der Programmiersprache Elm. Anlässlich des 100sten Puzzle Members haben sie ein Spiel entworfen, womit neu angestellte Members schneller die anderen Puzzlers kennen lernen können. In leicht abgewandelter und abgespeckter Form haben sie jetzt dieses Spiel auf Github veröffentlicht und nebenbei Charaktere, Raumschiffe und Fortbewegungsmittel aus dem StarWars-Universum kennen gelernt – ihre Erfahrungen mit Elm teilen sie in diesem Blogpost.


Geschrieben von Andreas Maierhofer und Max Burri


Elm ist eine Programmiersprache zum Entwickeln von Front-End Applikationen für den Browser. Als Punchline entnehmen wir der offiziellen Website folgendes:

delightful language for reliable webapps. Generate JavaScript with great performance and no runtime exceptions.

Die Versprechen sind ziemlich gewagt:

  • Reizend, angenehm (delightful) soll die Sprache sein
  • Zuverlässig (reliable) sind die Webapplikationen
  • No runtime exceptions – das haben wir gehört!
  • Grossartige Performance wird auch versprochen…

Einigen dieser Versprechen wollten wir für diesen Blog-Post nachgehen und haben dafür eine kleine Beispielapplikation entwickelt. Diese ist denkbar einfach: Der Spieler muss aus einer Auswahl von drei Vorschlägen den Namen der abgebildeten StarWars-Figur erraten.

Die StarWars-Kenntnisse können hier unter Beweis gestellt werden. Der Code ist Open Source: https://github.com/puzzle/character-guess. An dieser Stelle gehen wir nicht en Détail auf den Code der Applikation ein, sondern möchten einige Heraustellungsmerkmale der Sprache sowie unsere Eindrücke schildern.

Was unterscheidet Elm von Javascript?

Elm ist eine rein funktionale Programmiersprache. Ein Compiler übersetzt Elm-Programme in Javascript, Html und Css damit die Applikation im Browser lauffähig ist. Elm ist von Haskell inspiriert, vereinfacht aber zahlreiche Aspekte und bleibt so einsteigerfreundlich.

Die wichtigsten Eigenschaften der Sprache sind:

  • Streng statische Typisierung mit Type Inference
  • Immutable by default
  • Kein null oder undefined

Diese Eigenschaften erlauben es dem Compiler, der übrigens in Haskell geschrieben ist, die formale Korrektheit des Codes wesentlich tiefgreifender zu prüfen, als dies beispielsweise der Typescript- oder auch der Java Compiler kann. Insbesondere garantiert der Compiler, dass das ausgelieferte Programm keine Runtime Exceptions wirft…

Wirklich? No runtime Exceptions…?

Frontend-Entwicklern begegnen ihnen täglich und auch bekannte Websites sind nicht davor gefeit:

 

Elm ist streng statisch typisiert – und der Compiler kann jederzeit die Typen aller Ausdrücke ableiten. Aber warum ist das wichtig und was heisst das eigentlich?

Ein „strenges statisches“ Typen-System garantiert uns, dass ganze Kategorien von Fehlern nicht auftreten können – nämlich Ausdrücke die keinen Sinn machen. Zum Beispiel, wenn eine Zahl als Funktion verwendet werden soll oder wenn wir einer Funktion, die Zahlen erwartet, einen String füttern. Der Elm Compiler weigert sich, einen solchen Code zu compilieren und weist uns, freundlich aber bestimmt, auf unseren Fehler hin:

Diese ‚menschenfreundliche Fehlermeldungen‘ sind extrem hilfreich. Sie beschreiben genau welches Problem der Compiler erkannt hat. Bei komplexeren Problemen werden zudem noch Tipps, Beispiele oder Links zur Dokumentation inkludiert.

Ein weiteres Merkmal des Typen-Systems ist die Absenz von Null Werten. Dieser Billion-Dollar Mistake wird durch einen  Maybe Typ ersetzt. Der Compiler erkennt, dass der Wert potentiell nicht definiert ist und zwingt uns, diesen Fall zu behandeln.

Tatsächlich haben wir während der Entwicklung der Applikation nie eine Runtime Exception erlebt – dies ist auch die Erfahrung in grossen Projekten.

Ist das denn tatsächlich „reizend“ (delightful)?

Der Compiler schaut uns also ganz genau auf die Finger. Jeder Fehlerfall muss behandelt werden, und zwar jetzt und nicht erst später!

Auf den ersten Blick mag dies vielleicht wenig „delightful“ erscheinen, aber in der Praxis hat sich gezeigt, dass diese Strenge des Compilers zwei wichtige Vorteile bietet:

  1. Jede Fehlerbehandlung liefert einen Denkanstoss. Wie könnte der Laufzeitfehler vermieden werden? Habe ich die richtigen Typen in meinem Model? Fehlt dort etwas oder ist gar etwas zu viel? Dadurch erhält man ein stabileres und verständlicheres Model, woraus dann automatisch auch ein stabilerer und einfacherer Code entsteht.
  2. Bei jedem Refactoring bietet der Compiler seine Unterstützung an. Er liefert hilfreiche Fehlermeldungen und wird so zum Pair-Programming-Partner. Bei umfangreichen Refactorings steht er jederzeit bereit und führt uns Schritt für Schritt durch die notwendigen Anpassungen. Sobald der Code wieder compiliert, kann man sicher sein, dass die Applikation wieder formal funktioniert – ob es dann auch die Business-Logik tut, kann der Compiler natürlich nicht garantieren.

Zuverlässige Webapplikationen…

Dieser Punkt hat uns einiges an Kopfzerbrechen bereitet – was ist eigentlich eine zuverlässige Webapplikation? Reicht es, wenn sie macht, was sie soll – und zwar ohne Fehler? Wieso unterscheidet die Punchline („reliable webapps … without runtime exceptions“) explizit zwischen „zuverlässig“ und „ohne Fehler“?

Wir denken, dass „Zuverlässigkeit“ den gesamten Lebenszyklus der Wepapp betrifft, also Plan – Build – Run! Bei Puzzle setzen wir regelmässig Webapplikationen mit einer Lebensdauer von mehreren Jahren um. Oft macht die initiale Entwicklungsphase nur einen Bruchteil des gesamten Lebenszyklus aus – Wartung, Support und eventuell Weiterentwicklung sind genau so wichtig.

Das bedeutet für ein solches Projekt:

  • Die Teamzusammensetzung wird sich ändern, neue Members müssen eingarbeitet werden und sich v.a. auch in der Architektur und dem Code Style der Applikation zurecht finden.
  • Die eingesetzten Frameworks und Third Party Libraries entwickeln sich ständig weiter und sollten ohne grosse Aufwände nachgezogen werden können.

Unserer Erfahrung nach sind dies zwei „Pain Points“ bei Javascript Applikationen. Sowohl die Frameworks selbst, aber auch die Build Pipelines und das gesamte Ökosystem sind einem rasanten Wandel unterzogen. Die Architekturen unterscheiden sich von einem Projekt zum nächsten, auch bei gleichbleibenden Teams und Frameworks. Es wird heftig darüber diskutiert, ob und welches State-Management System (redux, ngrx, mobx, Context API,…) verwendet werden soll – und vor allem: Tabs vs. Spaces, Trailing oder Leading Commas?! Erschwerend kommt hinzu, dass viele der über 700’000 Packages auf npmjs.org es mit dem Semantic Versioning nicht so genau nehmen – leider. Durch die zahlreichen Abhängigkeiten der Packages – auch untereinander – führen Upgrades häufig zu Breaking Changes, auch in Minor oder Patch Releases – das ist übrigens im Java Umfeld nur wenig besser.

Elm bietet hier Lösungen:

  1. Der Paket Manager von Elm erzwingt echtes Semantic Versioning: Aufgrund der Typen und Signaturen der Funktionen in einem Modul kann die exakte neue Versionsnummer abgeleitet werden. Dadurch ist ausgeschlossen, dass Minor oder Patch Upgrades von Packages sogenannte „Breaking Changes“ enthalten – solche Upgrades können also bedenkenlos eingespielt werden.
  2. Es gibt nur eine Architektur für Elm-Applikationen – und diese ist direkt in die Sprache integriert, also nicht als Package zu installieren. Die Elm-Architektur war u.a. inspiration für Redux: „Redux evolves the ideas of Flux, but avoids its complexity by taking cues from Elm. Even if you haven’t used Flux or Elm, Redux only takes a few minutes to get started with.“ Quelle: https://redux.js.org/#influences

Auf die Details zur Elm-Architektur einzugehen würde den Rahmen dieses Posts sprengen, aber eine wichtige Konsequenz daraus ist: Die Elm-Architektur ermöglicht einen leichten Einstieg für Neueinsteiger oder bei der Übernahme bestehender Projekte, da der Programmfluss und Programmzustand festgelegt sind. Zudem wird diese Architektur immer angewendet, unabhängig von der Grösse der Applikation – die Frage ob man einen State Manager (bzw. welchen) verwenden will stellt sich in Elm nicht, wie in anderen Frameworks.

  1. Die Community hat sich auf einen einheitlichen Code-Style geeinigt: elm-format – ohne Konfigurationsmöglichkeit, mit einfacher Integration für die gängigen Editoren, kommt das Tool mit den abenteuerlichsten Formatierungen zurecht und rückt den Code – sofern er syntaktisch korrekt ist – wieder ins rechte Licht. Damit erhält man übrigens schon einen ersten Hinweis darauf, ob das, was man da geschrieben hat auch tatsächlich Sinn macht… besonders für Anfänger ist das sehr hilfreich und für den fortgeschrittenen Entwickler macht es einfach Spass, dass man sich nicht um die Formatierung kümmern muss.
  2. Aus unserer Sicht ebenfalls ein wichtiger Punkt: Seit dem letzten Elm-Release (0.18) sind jetzt zwei Jahre vergangen – im Javascript Ökosystem eine Ewigkeit. Für Elm aber sinnvoll – das klingt zwar merkwürdig, führt in letzter Konsequenz aber dazu, dass man eben nicht alle paar Monate den gesamten Tech-Stack nachziehen muss, obwohl die Neuerungen im Framework für die eigene Applikation vielleicht gar nicht hilfreich sind – diese Einschätzung teilen andere Teams mit relativ grossen Elm-Applikation in Produktion. (Hinweis: In der Zwischenzeit ist elm 0.19 erschienen).

Performance

Für den Endbenutzer entscheidend ist schliesslich die Performance. In dieser Hinsicht muss man sich bei Elm keine Sorgen machen. Obwohl schon etwas älter zeigt der Vergleich von 2016, dass die Performance von Elm-Applikationen durchschnittlich besser war als mit den damals aktuellen Versionen der vergleichbaren Javascript Frameworks – vor allem auch mit dem damaligen Branchenprimus Angular. Die Konkurrenz hat in der Zwischenzeit sicherlich nachgezogen – verstecken muss sich Elm in dieser Hinsicht aber immer noch nicht.

Fazit

Die Programmiersprache Elm bietet zuverlässige Lösungen für zahlreiche Probleme der Frontend-Entwicklung. Ein direkter Vergleich mit anderen Frontend-Frameworks ist nur bedingt sinnvoll, weil die Ausgangslage völlig unterschiedlich ist: angular, react etc. bauen auf Javascript auf. Eine Programmiersprache mit anderen, verschiedenen Paradigmen und leiten daraus auch ihr Ökosystem und ihre Architekturen ab. Weil Javascript wesentlich populärer ist, steht für die entsprechenden Frameworks eine unüberschaubare Menge an Packages zur Verfügung, wo man bei Elm noch selber Hand anlegen muss. Aufgaben, die in Javascript trivial sind (z.B. Zufallszahlen) erfordern in Elm schon einige Konzepte mehr. Zudem wird den Entwicklern viel Freiheit entzogen: die Applikationsarchitektur ist vorgegeben, der Code Style ist fix und man muss sich um alle möglichen und eigentlich unmöglichen Fehlerfälle kümmern – es scheint, als ob man viel Kreativität aufgibt und in ein Korsett gezwungen wird.

Was man dafür erhält?

Spass am Entwickeln von Features – es ist eben unglaublich befreiend wenn man sich nicht ums Tooling kümmern muss, wenn die Architektur klar ist, die Diskussion um den Code Style entfällt. Man gewinnt Sicherheit bei Anpassungen und Erweiterungen: Wenn der Compiler zufrieden ist, dann funktioniert alles wieder. Wenn wir in Elm entwickeln, müssen wir nie Debuggen (das geht auch gar nicht) oder in verschachtelten Stacktraces in den Devtools des Browsers nach dem Fehler suchen, um dann präsentiert zu bekommen:

undefined is not a function (main.js: 1)

Oft kann man relativ lange Zeit, ja ganze Tage weiterentwickeln oder Refactorings machen, ohne dass man die Applikation im Browser nachlädt: Wenn der Compiler sein OK gibt, dann funktioniert es auch! Das ist unsere Erfahrung.

Unsere persönliche Meinung ist klar: Die „Developer Happiness with Elm“ ist derart gut, dass wir uns weiter damit beschäftigen und hoffentlich auch weiter darüber bloggen können.

Für weitere Informationen empfehlen wir die folgenden Talks:

Empfohlene Resourcen

Kommentare sind geschlossen.