Elm – Brauche ich das als JavaScript-Entwickler?

Mika
Mika, Frontend Development
21 Aug 2019
Elm – Brauche ich das als JavaScript-Entwickler?

Ein guter Freund hat mir letztens ans Herz gelegt, dass ich doch wirklich mal Elm lernen solle, denn: “Du wirst Einiges daraus mitnehmen können – egal, ob Du es letzten Endes regelmäßig nutzt oder nicht.“

Natürlich gehört besagter Freund zu jener Sorte Mensch, die immer überzeugend klingt – völlig egal, was sie gerade empfiehlt. Könnte an seinem majestätischen weißen Bart oder den stechend blauen Augen liegen. Oder vielleicht doch eher an der Tatsache, dass dieser besagte Freund meiner Erfahrung nach quasi immer recht hat.

Elm steckt mit seinen vier Jahren auf dem Buckel schon lange nicht mehr in den Kinderschuhen. Falls der Begriff Elm bei dir noch Stirnrunzeln und Fragezeichen hervorruft, hier eine kleine Nachhilfestunde: Elm ist eine funktionale Programmiersprache, die zu JavaScript kompiliert und die bei der Auswahl der „richtigen“ Frameworks glücklicherweise nicht dieselbe Schwerfälligkeit und Komplexität aufweist wie J – weil Elm im Grunde genommen selbst das Framework ist.

Elm will erreichen, dass komplizierte Abhängigkeiten und das mühsame Einfügen von Zusatzbibliotheken in JavaScript endgültig der Vergangenheit angehören – und das gelingt recht erfolgreich, denn Elm, das auch oftmals als der Vater von Redux bezeichnet wird, ist absolut funktional.

elm-graphik

Nachdem ich diese grundlegenden Informationen eingeholt hatte, war die Entscheidung mehr oder weniger schnell war getroffen: Ich würde tapfer mehrere vielversprechende Wochenendaktivitäten fernab des Computers opfern, um mir stattdessen eine gehörige Portion Insiderwissen über Elm anzueignen. Und weil ich ja nicht so bin, teile ich diese neuen Einblicke natürlich freudig mit euch:

1. Keine Runtime Errors

Vermutlich ist es am besten, wenn ich mich gleich kopfüber in die wichtigsten Konzepte von Elm stürze.

In Elm wirst du dich nicht ständig mit nervigen Laufzeitfehlermeldungen a la `NaN`, ‘Uncaught TypeError’ oder ‘foo is undefined’ herumschlagen müssen, denn Elm unterstützt die sogenannte statische Typisierung (engl. „static type checking“), was bedeutet, dass zum Zeitpunkt der Kompilierung dein gesamter Code überprüft wird, um sicherzustellen, dass sämtliche Variablen und Inputs/Outputs der verwendeten Funktionen reibungslos miteinander funktionieren. Wenn deine Funktion jetzt beispielsweise nur String akzeptiert, bedeutet das, dass du unmöglich irgendetwas anderes dazwischen schummeln kannst.

Und obwohl gemeinhin empfohlen wird, die Typen immer wie folgt anzugeben:

reps : String -> Int

(reps ist hierbei der Name der Funktion, String ist der Input-Typ und Int beschreibt den Output-Typ),

… ist dies nicht unbedingt erforderlich, da Elm in der Lage ist, automatisch sämtliche vorliegenden Typen zu identifizieren (im Fachjargon nennt man diesen Vorgang übrigens Typ-Inferenz). Wenn die Kompilierung abgeschlossen ist, wird dir Elm eine entsprechende Erfolgs- oder Fehlermeldung anzeigen.

2. Union Types

Ein guter Trick, der von vielen Elm-Nutzern häufig angewendet wird, ist der sogenannte Union Type, der dem Nutzer die Möglichkeit bietet, mögliche Werte für komplexe oder uneinheitliche Typen zu definieren. Nehmen wir hier z.B. an, dass wir es mit einem Exercise Typ zu tun haben, der von einem dieser beiden Strings repräsentiert werden kann: Pull-up oder Push-up

type Exercise = Pull-up | Push-up

und jetzt möchten wir eine reps Funktion erstellen, die basierend auf dem Exercise Typ eine gewisse Anzahl von reps zurückgibt:

reps : Exercise -> Int
reps exercise =
case exercise of
Pull-up ->
10

Push-up ->
15

Der relevante Faktor ist hierbei, dass wir gezwungen sind, den case Ausdruck zu verwenden, wann immer wir eine Union Variable als Parameter (in diesem Beispiel wäre das exercise) anwenden wollen. Noch wichtiger ist allerdings die Information, dass alle verschiedenen Bereiche dieses case Ausdrucks vom Compiler unter die Lupe genommen werden. Was in weiterer Folge bedeutet:

  • Dass wir zum Zeitpunkt der Kompilierung eine Fehlermeldung erhalten werden, wenn wir vergessen haben, einen Case zu bedienen (wie z.B. Muscle-up mit 8 reps).
  • Dass wir zum Zeitpunkt der Kompilierung eine Fehlermeldung erhalten werden, wenn wir uns aus Versehen beim Name eines Case vertan oder vertippt haben (wie z.B. Psuh-up)
    • Ziemlich cool, oder?

3. Edge cases

Das war´s aber noch nicht mit den verschiedenen Typen!

Bevor ich persönlich in die Welt der Entwickler vorgestoßen bin, habe ich mehrere Jahre damit zugebracht, Software Requirement Specifications zu verfassen. Das bedeutet, dass mir die Bedeutsamkeit von Edge Cases sehr wohl bewusst ist– und dennoch war ich bereits das eine oder andere Mal kurz davor, genau diese Edge Cases in meinem eigenen Code zu vergessen. Was wäre z.B., wenn der Wert undefined bleibt, das JSON-Format inkorrekt ist oder für den HTTP-Request ein Timeout vorliegt?

Elm hat sich diesen kleinen (oder für manche Menschen großen, wenn nicht sogar „nervenzusammenbruchinduzierenden“) Problemchen angenommen und dazu Maybe, Result und Task eingeführt, die im Grunde genommen special cases von bereits bekannten Union Types darstellen.

Gibt es in deinem Code ein optional field, wirst du höchstwahrscheinlich Maybe anwenden wollen:

type alias Sportsman =
{ name : String
, age : Maybe Int
}

… was bedeutet, dass das age field einen von 2 Werten annehmen kann: Nothing oder Just Int – bei Ersterem handelt es sich um einen leeren Wert, der zweite ist nicht-leer. Da es sich wie bereits erwähnt um einen Union Wert handelt, gilt darüber hinaus noch Folgendes: Jedes Mal, wenn du diesen VAR als Parameter anwendest, bist du gezwungen, den case Ausdruck zu verwenden und sämtliche Variationen zu behandeln, damit du null/undefined cases nicht vermeiden kannst:

getReps : Sportsman -> Maybe Int
getReps sportsman =
case sportsman.age of
Nothing ->
Nothing

Just age ->
Just 12

Derselbe Ansatz gilt für Result und Task Typen, allerdings wird Task nicht bei optional fields verwendet, sondern bei Sync Operations, wie z.B. beim Parsen von Json. Result kommt hingegen bei Async Operations zur Anwendung, wie z.B. bei HTML-Requests. Beide Typen können im Endeffekt jeweils einen der folgenden zwei Werte annehmen: Success für erfolgreiche Fälle und Failure für Error-Fälle – und analog zu Maybe gilt auch hier, dass wir immer gezwungen sind, beide Fälle zu behandeln.

4. Package Distribution

Eine weitere zwingende Konsequenz der statischen Typisierung ist die automatisch durchgeführte Versionierung.

Wenn wir beispielsweise eine Library erstellen und diese mit anderen teilen wollen, bringt uns das den Vorteil, dass wir uns nicht jedes Mal, wenn wir das Package updaten wollen, den Kopf über die aktuell gültige Versionsnummer zerbrechen müssen. Elm nimmt uns diese Arbeit ab.

Beginnend bei der ersten Version 1.0.0 wird Elm die Code-Änderungen für alle nachfolgenden Releases unseres Pakets vergleichen und automatisch eine neue Versionsnummer erzeugen, die die Änderungen, die wir vorgenommen haben, am akkuratesten widerspiegelt. ?

5. Redux

Redux ist ein weithin bekanntes Muster in Javascript, das zum Teil Elm entsprungen ist. Redux kommt beispielsweise in React und Angular vor, ich persönlich habe es kürzlich sogar in einem jQuery-Projekt ohne jegliche moderne MV* Frameworks genutzt.

Die Idee dahinter ist, dass man einen einzigen Point of Trust hat, der je nach Variante den Namen Model(Elm), State (React) oder Store (Angular) trägt und für den es ein Interface für Updates gibt. In Elm wird dieses Interface Update genannt (in der Javascript-Welt wäre das der Reducer).

Jedes Mal, wenn wir das Model verändern wollen, rufen wir daher die Update Funktion auf.

Die Update Funktion kennt aktuell Model und Msg (Action) als Parameter und gibt das neue Model zurück.

Seit geraumer Zeit können wir Model verwenden, um unsere Ansicht (View) upzudaten, was in Sachen Websiten natürlich Html ist. Dazu müssen wir nichts weiter tun, als die selbst betitelte Funktion  View aufzurufen.

Das Ganze sieht dann wie folgt aus:

elm-graphik1

Die Elm Architektur

Und warum finde ich das alles so cool?

6. QA und Support

Zuallererst möchte betonen, dass QA (also Quality Assurance) mit Elm wesentlich unkomplizierter und zeitsparender wird.

Wir wissen bereits, dass Elm eine funktionale Programmiersprache ist, was bedeutet, dass sie uns 2 funktionale Garantien bereitstellt:

  • Alle Funktionen produzieren immer dieselben Resultate (wenn dieselben Argumentwerte verwendet werden).
  • Alle Funktionen verursachen keine Nebeneffekte, das heißt, dass sie außerhalb ihres individuellen Geltungsbereichs nichts verändern.

Im Grunde genommen bedeutet das, dass wir eine Liste mit Update-Funktionsaufrufen immer und immer wieder anwenden können und als Resultat immer ein- und dasselbe Model zurückerhalten werden. Warum? Weil nichts und niemand unser Model von außen ändern kann (erinnerst du dich, keine Nebeneffekte?) und weil unsere Update Funktion immer dasselbe Resultat zurückgibt, wenn dieselben Parameter angewendet wurden.

In weiterer Folge heißt das, dass dir dein QA Engineer jedes Mal, wenn er über einen Bug stolpert, einfach die Logs aller Update-Funktionsaufrufe (die im Hintergrund aufgezeichnet werden) zusendet, ohne manuell alle diese Schritte aufzeichnen zu müssen. Wir selbst erhalten dadurch ebenfalls den Vorteil, dass wir nicht mehr manuell sämtliche Schritte reproduzieren müssen. Es genügt, wenn wir einfach alle Schritte in die App importieren, danach wird der Bug sofort sichtbar werden. Außerdem ist es möglich, im Aktionsprotokoll vor- und zurückzugehen, damit sofort deutlich wird, welche Kette von Nutzeraktionen zu besagtem Fehler geführt hat. ??

6. Lazy Loading

Der zweite große Vorteil von Redux ist Lazy Loading.

Einer der größten Vorteile von modernen Javasript Frameworks im Vergleich zu jQuery ist meiner Meinung nach, dass sie deklarativ sind. Dabei muss man dem Programm nicht mehr im Detail vorbeten, wie es den DOM manipulieren soll – also append, remove, hide, etc. Stattdessen beschreibt man nur noch den anfänglichen und finalen Zustand des HTML – also wie es in verschiedenen Stadien aussehen soll – und das Framework kümmert sich um den Rest (für gewöhnlich durch Virtual DOM), führt also automatisch alle notwendigen Updates durch, damit die Seite im Endeffekt so aussieht, wie du sie dir vorgestellt hast.

Dasselbe Prinzip gilt für Elm. Man definiert einfach die View Funktion und beschreibt im Detail und basierend auf dem aktuellen Model, wie HTML gerendert werden soll. Sobald das Model abgeändert wurde, wird die View Funktion automatisch ein dementsprechendes Update des DOM durchführen, wobei ein Vergleich der aktuellen und vorherigen Stadien und die dementsprechenden Manipulationen des DOMs durchgeführt werden.

Nun zu einer interessanten Frage: Was passiert, wenn nur ein kleiner Teil des Models ein Update erhalten hat und der Rest unverändert blieb? Macht es dann Sinn, alles neu zu berechnen und den gesamten DOM zu vergleichen? Die richtige Antwort kannst du dir bereits denken: Mit Sicherheit nicht.

Wenn wir unsere View Funktion in kleine Teile aufbrechen, wodurch eine Funktion den Header rendert, eine weitere rendert den Footer etc., können wir aufgrund der ersten Funktionsgarantie, die wir weiter oben bereits erwähnt haben (garantiert dasselbe Resultat, wenn dieselben Argumentwerte verwendet wurden), all jene Funktionen überspringen, deren Inputs nicht verändert wurden.

In diesem Fall stellen wir einfach das Keyword lazy ganz vorne vor unsere Funktion:

viewHeader: String -> Html Msg
viewHeader name =
…some code

view : Model -> Html Msg
view model =
lazy viewHeader model.loggedUser.name
…other code

In obigem Beispiel haben wir einen Header, der den eingeloggten Usernamen anzeigt. Sobald wir irgendetwas im Model verändern (der Nutzer scrollt nach unten und neuer Content wird geladen), bringt es uns nichts, den Header-Bereich des DOMs neu zu berechnen, da dieser völlig unverändert geblieben ist. Allerdings möchten wir den Content-Teil neu berechnen und updaten, da dieser vom User nach unten gescrollt und somit verändert wurde. Und hier kommen die grandiosen Eigenschaften von lazy ins Spiel – Elm wird überprüfen und erkennen, dass der view Header Input nicht verändert wurde, wodurch ein Update des Header-Teils übersprungen, der Rest aber ganz normal gerendert wird. Dadurch können wir wertvolle CPU-Ressourcen sparen und das Rendering wird erheblich schneller vonstattengehen.

7. Immutability

Dies führt uns zu einem weiteren wichtigen Konzept von Elm, nämlich immutability (Unveränderlichkeit). In früheren Beispielen nutzten wir String als Input-Typ für unsere Header Funktion. Was aber, wenn wir es mit einem komplexeren Datentyp zu tun haben – wie z.B. Object?

Was, wenn wir zwei verschiedene Input Objekte (neu und alt) vergleichen möchten? In diesem Fall müssten wir in JavaScript tief in die Fields Hierarchie vordringen und dabei ein Feld nach dem anderen vergleichen. Zeit- und ressourcensparend geht anders.

Mit immutability sieht die Sache allerdings ganz anders (und besser) aus: Immutability setzt voraus, dass jedes Mal, wenn wir etwas verändern, ein neues Objekt erstellt wird. Ein Vergleich zwischen alten und neuen Versionen ist dadurch nicht mehr als ein Vergleich von Referenzen – was rasend schnell geht. Wenn die Referenzen auf dasselbe Objekt hindeuten, dann handelt es sich auch um dasselbe Objekt. Falls nicht, haben wir es mit zwei unterschiedlichen Objekten zu tun. Wie cool, oder? Mit diesem neuen Wissen macht Lazy Loading noch mehr Sinn.

8. Noch schneller

Aber wir können die Geschwindigkeit sogar noch weiter verbessern.

Wir alle wissen, dass ein typischer Browser 60 Bilder pro Sekunde darstellt, da unser Auge exakt mit dieser Frequenz visuelle Informationen wahrnimmt. Aus diesem Grund ergibt es überhaupt keinen Sinn, mehr Bilder pro Sekunde unterbringen zu wollen – da das menschliche Auge die zusätzliche Information gar nicht verarbeiten kann, wirkt eine Website mit höherer Bildfrequenz niemals besser. Der einzige Effekt ist also eine Verschwendung von Geräteressourcen.

Gleichzeitig möchten wir natürlich sicherstellen, dass andere Operationen abseits der Bildfrequenz im Normalmodus ablaufen, sei es ein HTTP-Request oder komplexe Berechnungen im Hintergrund.

Wie können wir das in Elm erreichen?

Im Grunde genommen brauchen wir dazu gar nichts tun, denn diese Eigenschaft ist bereits in Elm integriert. Elm verwendet requestAnimationFrame , um eine Synchronisation der view Berechnungen und der Bildfrequenz des Browsers durchzuführen. Das bedeutet, dass die view Funktion, die HTML rendert, nicht öfter als 60 Mal pro Sekunde aufgerufen wird, was Zeit, CPU-Ressourcen und Batterieleben des mobilen Endgeräts spart.

Der Hauptgrund, warum das in Elm möglich ist, ist die zweite Funktionsgarantie, die auf die view Funktion angewendet wird – es gibt keine Nebeneffekte, daher ist HTML der einzige Bereich, der verändert wird. Das bedeutet, dass Model dadurch nicht upgedatet werden kann. Während wir bestimmen, dass nur 60 view Funktionsaufrufe pro Sekunde stattfinden, wird unser Model dennoch in Echtzeit und ohne Verzögerungen upgedatet. Reiner kann eine Funktion gar nicht sein.

9. Fuzz Testing

Wie wir bereits festgestellt haben, verwendet Elm ziemlich viel Zeit darauf, Fehlermeldungen in unseren Applikationen vorzubeugen, weshalb das verwendete Test-Framework ebenfalls relativ aufwendig und umfangreich ist.

Ein Feature, das in mir persönlich besonders große Begeisterung auslöst und das bald in Mocha erscheinen wird, ist Fuzz Testing.

Die Idee dahinter ist simpel, aber wirkungsvoll: Ein Testcase wird mehrere Male mit verschiedenen, zufällig generierten Inputs ausgeführt. Besonders praktisch ist daran, dass sämtliche Edge Cases, wie z.B. zero, empty oder alle negativen Werte, garantiert mindestens einmal überprüft werden. Außerdem haben wir die Möglichkeit, neben den Wahrscheinlichkeiten der Edge Cases auch verschiedene Reichweiten zu definieren, wodurch sichergestellt wird, dass die wichtigsten Fälle öfter getestet werden als andere.

Fuzz.frequency
[ ( 1, Fuzz.intRange -100 -1 )
, ( 3, Fuzz.intRange 1 100 )
]

Dadurch können wir uns extrem viel Zeit sparen, die wir andernfalls damit zubringen würden, manuelle Tests zu schreiben. Einen kleinen Nachteil gibt es allerdings: Die Durchführungsdauer der Tests kann merklich länger sein.

Fazit

Nun, da wir uns all dieses großartige Wissen angeeignet haben, stellt sich natürlich die Frage, wie wir es in unserem normalen Leben als Javascript-Entwickler nutzen können. Daher hier noch einmal kurz und bündig:

Tipp Nummer 1: Nutze statische Typisierung – Typescript oder Flow wären beispielsweise großartige Lösungen. Typescript erlaubt uns sogar eine Kombination mit Javascript in ein- und derselben Codebasis, was bedeutet, dass wir unser Projekt Schritt für Schritt in statische Typisierung übertragen können, ohne dabei Gefahr zu laufen, den Code des Altsystems zu zerstören. In Kombination mit VSCode profitieren wir außerdem von großartigem IntelliSense Support.

Tipp Nummer 2: Nutze, wenn angebracht, eine funktionale Herangehensweise (State Management, Rendering etc.). Die Redux Library ist hierfür ein großartiges Beispiel.

Tipp Nummer 3: Nutze Immutability mithilfe von gebrauchsfertigen Libraries wie z.B. Mori oder immutable.js, um die Erkennung von Veränderungen im Code voranzutreiben und die Performance deiner Applikation zu verbessern.

Immutability


Haben Sie Fragen oder benötigen eine individuelle Beratung zu dem, was Sie gerade gelesen haben? Zögern Sie nicht, uns zu kontaktieren! Besuchen Sie unsere Kontaktseite und lassen Sie uns ein Gespräch darüber beginnen, wie wir Ihnen helfen können, Ihre Ziele zu erreichen.

Mucki

Jetzt Beratungsgespräch vereinbaren:

Marcus Renz

Geschäftsführung

+49 711 217258 42

Email schreiben