en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

ManuScripts: WJERT – FP mit Elm – Teil 6 – Elm rennt

Eine Blog-Serie zum Vergleich von Elm, ReactJS und AngularJS anhand eines praktischen Beispiels.Hier geht es direkt zu [Teil 1, 23, 4, 5]

Herrlich frische Bergluft bläst unseren Wandervögeln entgegen auf der Ämpächli Alp, wo Angular sich vor allem über den festen Boden unter seinen Füssen freut, sowie die Aussicht auf eine Erfrischung im Restaurant bei der Bergstation. Elm ist aber überhaupt nicht in der Stimmung, bereits eine Rast einzulegen. Sie einigen sich auf einen Treffpunkt: Hängstboden. Elm marschiert wehenden Schrittes los, mit gewölbter Brust, den Kopf der Sonne entgegengestreckt.

Zur Erinnerung: Das Endziel ist ein Anagram-Generator für die deutsche Scrabble-Community.

Etappenziel: Interaktive Benutzeroberfläche

Vorbereitung (create-elm-app)

In den letzten Monaten ist die Elm-Community sehr weit gewandert und nebst zahlreichen Editor-Plugins für die gängigen Opensource-Editoren sind andere wertvolle Tools erschienen, wie z.B. create-elm-app, welches sich webpack bedient, und damit eine «state-of-the-art» Toolchain für die Entwicklung unter elm bereitstellt.

Zur Erinnerung: Elm-Programme werden mit dem Compiler elm-make nach javascript kompiliert und das wiederum wird in eine Html-Seite eingebunden, entweder in ein Ziel-div oder fullscreen oder sogar headless (Keine Anzeige).

Der mitgelieferte elm-reactor (development web-server) reicht für die Entwicklung eigentlich aus, aber mit webpack gibt es jetzt schon solche goodies wie «hot reload» und css stylesheets können so auf die altbekannte Weise eingebunden werden, was für den Anfang auch vortheilhaft ist.

Dadurch bleibt mehr Zeit für die Sache, die Spass macht: die Programmierung mit Elm. Wer später seinen eigenen Build-Prozess einführen möchte, kann die Abhängigkeit problemlos durch das Tool entfernen lassen.

Die Installation erfolgt auf der Kommandozeile: sudo npm install -g create-elm-app

Wir fangen also nochmals ganz von vorne an mit unserer Applikation und kopieren die Models aus den vergangenen Blog-Posts bei Bedarf in unser Projekt. Zuerst aber mal eine neue elm-app erstellen:

create-elm-app /path/to/my/project/elm-anagrams

Creating elm-anagrams project...
 
Packages configured successfully!
 
Project is successfully created in `/path/to/my/project/elm-anagrams`.

cd /path/to/my/project/elm-anagrams

elm-app start

Nach kurzer Zeit erscheint:

Compiled successfully!
 
The app is running at:
 
    http://localhost:3000/
 
To create production build, run:
 
    elm-app build

und zusätzlich wird der Standard-Browser unter obiger Adresse geöffnet, wo nochmals eine zufriedenstellende Erfolgsmeldung angezeigt wird.

 

Editor: Atom, Brackets, Emacs, IntelliJ, Sublime Text, Light Table, Vim, VS Code

Hier ist die Feature-Matrix

Persönlich nutze ich im Moment Atom mit folgenden Plugins:

Ich wechsle demnach (in einer neuen Konsole) ins elm-anagrams-Verzeichnis und starte atom .

Projekt-Struktur

elmproject

create-elm-app legt die Ordner src und tests an. Was durch elm-make kompiliert wird, landet im Verzeichnis elm-stuff. Das letzte Verzeichnis, dist, wird vom Build-Tool erzeugt. Ausserdem wird gleich auch noch die korrekte .gitgnore-Datei erzeugt, so dass wir nur noch git init && git add . && git commit -m «initial» aufrufen müssen, um unsere Resultate unter Versionskontrolle zu stellen.

Damit wir sofort loslegen können, öffnen wir die Datei src/App.elm, wo wir den folgenden Inhalt vorfinden:

module App exposing (..)
 
import Html exposing (text, div)
 

subscriptions model =
    Sub.none
 
 
update msg model =
    ( model, Cmd.none )
 
 
init =
    ( (), Cmd.none )
 
 
view model =
    div [] [ text "Your Elm App is working!" ]

Ich würde die Reihenfolge init, subscriptions, update, view bevorzugen (und rearrangiere entsprechend), aber dies bleiben die vier Bausteine, die durch die TEA (the elm architecture) vorgeschlagen werden und welche die gesamte Applikation ausmachen. Subscriptions verwenden wir vorerst nicht.

Wir erinnern uns daran, dass elm mit einem immutable (unveränderbaren) Model operiert. Das Modell wird immer an die betreffenden Funktionen übergeben und daraus wird (in den meisten Fällen) ein neues Modell resultieren.

Die folgende Grafik veranschaulicht dies und beschreibt die Funktionsweise von elm.

Update-Loop

@fredcy’s update loop:

Here is a simplified model of how an Elm 0.17 program operates.

The update function blocks waiting for the next incoming Msg. When received it creates a new model value and commands. The view and subscriptions functions then run against the new model value, producing new Html Msg and Sub Msg values, respectively. Then update waits for the next Msg. Note that subscriptions is called after each update.

elm update loop. thanks to @fredcy!

Happy Elming!

So, hier noch der Link zum Syntax, und dann legen wir los:

Das Ziel

Unser heutiges Ziel ist es, eine einfache Eingabe-Maske zu erstellen, welche ein Textfeld enthält, welches bei Eingabe eines Zeichens den Bereich darunter entsprechend aktualisiert. Zu einem späteren Zeitpunkt befassen wir uns mit dem HTTP-Aufruf ans Backend.

UI mock

init und Model

Model the problem!

Zuerst soll man sein Problem modellieren. Welche Daten muss unser Modell enthalten, damit wir unser Ziel erreichen können?

Dazu erstellen wir im File App.elm zwei neue Typen:

Eine erste Version könnte folgendermassen aussehen:

type alias Anagram = String   -- bloss ein neuer Name für String, lesbarer, lässt einfachen Ausbau zu
 
type alias Model = 
  { input: String             -- der momentane String im Textfeld
  , anagrams: List Anagram    -- die Liste der aktuellen Resultate
  }

Von diesem Typ wollen wir nun eine Initialversion erstellen in der Funktion init. Diese sieht neu, mit Signatur, folgendermassen aus:

init: (Model, Cmd Msg) 
init = ( { input = "", anagrams = [] }, Cmd.none )

Wir ersetzen das bisher leere Model () vom Typ Unit durch einen record ({ input = «», anagrams = [] }), der mit dem deklarierten Typ vollständig übereinstimmt und erst dadurch kompilieren kann. Wenn die Reihenfolge der Argumente eingehalten wird, kann das Model auch so erzeugt werden (Record-Konstruktor-Aufruf): Model «» []. Es müssen jedoch immer alle Felder initialisiert werden.

() kann als ein Tupel ohne Elemente betrachtet werden, oder ein Wert ohne Daten.

Cmd und Msg

Der Rückgabe-Wert von init ist ein Tuple tup = ( x, y ), welches an erster Stelle (fst tup) unser Modell enthält, und an zweiter Stelle (snd tup) stehen allfällig auszuführende Kommandos, die Funktionen mit Seiten-Effekten betreffen. elm kontrolliert die Aufrufe solcher Funktionen explizit, damit es nicht zu Laufzeitfehlern kommt. Der Entwickler wird dadurch gezwungen, auch den möglichen Fehlerfall im Code abzudecken. Ersichtlich wird das am Typ Task, der für den Aufruf solcher Funktionen verwendet werden muss:

type alias Task err ok = 
    Task err ok

Um einen Task erstellen zu können, müssen zwei Argumente (Funktionen) übergeben werden. Das erste behandelt den Fehlerfall, das zweite den Erfolgsfall mit dem angeforderten Resultat. Funktionen, die Seiten-Effekte aufweisen, sind z.B. Http.get oder Date.now.

Jedes Cmd spezifiziert:

  1. Auf welche Effekte man zugreifen möchte und
  2. Welche Nachrichten in die Applikation zurückgelangen.

Und obwohl wir in der Signatur von init angegeben haben, dass wir Nachrichten vom Typ Msg zurückhaben wollen, haben wir diesen Typ nirgends spezifiziert. Zeit, dies nachzuholen.

type Msg 
  = NoOp

Ich habe hier eine Nachricht deklariert, welche dem Namen nach, keine Operation zur Folge hat. Unter gewissen Umständen kann das von Nöten sein, hier wird es verwendet, um eine Fähigkeit des Typ-System von elm zu zeigen. Denn wenn nebst NoOp noch andere Nachrichten erwarten, wie z.B. wenn der Benutzer Text ins Textfeld eingibt oder löscht, dann können wir diese Information an diesen Typ anhängen, was dann so aussieht:

type Msg 
  = NoOp 
  | OnInputChanged String

Dieses Konstrukt nennt sich Union Type und ist eins der mächtigen Werkzeuge der funktionalen Programmierung. Damit können auf natürliche Weise auch komplexe Strukturen ausgedrückt werden. Union Types werden oft auch tagged unions genannt, oder auch ADTs (algebraic data types). Sie beherbergen noch sooo viel mehr, wie der Begriff algebra in ADT vermuten lässt. Dieses höchst spannende Thema muss aber noch warten, weil hier geht es um elm, und bei elm lautet das Motto: «Let’s build stuff».

Für den Moment reicht es, zu erkennen, dass ein Wert vom Typ Msg entweder vom Typ NoOp (Nachricht ohne zusätzlichen Wert) sein kann, oder vom Typ OnInputChanged mit einem zusätzlichen «Payload» vom Typ String

view

Nun können wir eigentlich bereits unsere View erstellen. Mit voller Signatur lautet diese im Original:

view: Model -> Html Msg 
view model =
    div [] [ text "Your Elm App is working!" ]

Die view-Funktion erhält ein Model und gibt ein Konstrukt vom Typ Html Msg zurück, also Html, welches Nachrichten vom Typ Msg erzeugen kann.

Elm deckt im Modul Html grosse Teile der Html5-Funktionalität ab (mit wenigen, z.T. esoterischen Ausnahmen). Wenn ein Html-Element erzeugt werden soll, dann haben die Funktionen, die das ermöglichen, die folgende Signatur (hier am Beispiel h1):

h1 : List (Attribute msg) -> List (Html msg) -> Html msg

Nehmen wir das folgende Html an: <h1 class=»special» z-index=»50″><span>Bingo</span><span>Mania</span></h1>

Ein Html-Element hat 0-n Attribute (class, z-index), sowie eine Liste von enthaltenen, oder Kind-Elementen, wie man das von einer Baumstruktur gewohnt ist. Die Funktions-Signatur drückt genau dasselbe aus: h1 ist der Name einer Funktion mit zwei Argumenten. Beide Argumente sind Listen, die erste enthält jedoch Werte vom Typ Html.Attribute msg , die zweite Werte vom Typ Html.Html msg. Hinweis: die kleingeschriebenen Typnamen (hier msg), weisen auf Platzhalter für beliebige Typen hin, in unserem Fall heisst der konkrete Typ ja Msg (Typnamen sind immer upper case).

In elm sähe dasselbe demnach so aus: h1 [ HA.class «special», HA.zIndex «50» ] [ span [] [text «Bingo»], span [] [text «Mania»] ]

Hinweis: Bestehendes Html kann mit dem Paket mbylstra/html-to-elm nach elm konvertiert werden. Demo

Zurück zum Anagrammerator: Hier ist der Code für die View, wie sie im Bild oben angedeutet ist, ganz ohne zusätzlichen Schnickschnack:

import Html.Attributes as HA
 
view : Model -> Html Msg 
view model =
    div []
        [ Html.h1 [] [ text "Anagrammerator" ]
        , Html.h2 [] [ text ("Input: " ++ model.input) ]
        , viewTextField model
        , viewResults model
        ]
 
 
viewTextField : Model -> Html Msg 
viewTextField model =
    div [ HA.class "text-field-area" ]
        [ Html.span []
            [ Html.label [ HA.for "input" ] [ text "Buchstaben auf Bank" ]
            , Html.input
                [ HA.id "input"
                , HA.type' "text"
                ]
                []
            ]
        ]
 
 
viewResults : Model -> Html Msg 
viewResults model =
    div [ HA.class "result-area" ]
        [ Html.h2 [] [ text "Resultate" ]
        , Html.ul [] (List.map viewAnagram model.anagrams)
        ]
 
 
viewAnagram : Anagram -> Html Msg 
viewAnagram anagram =
    Html.li [ HA.style [ (,) "font-weight" "bold" ] ] [ text anagram ]

 

preview and review

Wer seinen Browser beim Entwickeln betrachtet hat, wird feststellen, dass sich dieser bei jeder Änderung automatisch aktualisiert, ohne seinen Zustand zu verlieren. Falls die app nicht läuft, diese wieder starten, und im Browser den Output betrachten. Wer unbedingt muss, kann nun auch etwas «live» stylen, inder er main.css anpasst und speichert.

Wir stellen aber fest, dass hier noch nichts von Bedeutung geschieht. Eingaben im Textfeld werden nicht sichtbar.

Eine erste Änderung könnte nun so aussehen:

Html.input
  [ HA.id "input"
  , HA.type' "text"
  , HA.value model.input   -- setze den Wert des Input-Felds auf den Wert des Feldes `input` im Model.
  ] []

Wenn man nun versucht, ins Feld zu tippen, wird der Wert beim nächsten Zeichnen gleich wieder zurückgesetzt, was sofort stattfindet. Auch nicht so nützlich. Damit der Wert ins Model gelangt, müssen wir mit der folgenden Änderung das Ereignis abfangen und in eine Nachricht unseren Typs verwandeln:

import Html.Events as HE
 
...
  , Html.input
      [ HA.id "input"
      , HA.type' "text"
      , HA.value model.input
      , HE.onInput OnInputChanged  -- übersetze das Ereignis in eine Nachricht
      ]
      []

Eventhandler-Funktionen sind im Modul Html.Events implementiert. Darum wird dieses importiert und mit Alias versehen. Aus diesem Modul verwenden wir die Funktion

onInput : (String -> msg) -> Attribute msg

Diese Funktion hat einen Eingabe-Parameter, welcher eine Funktion ist, die einen String kriegt und einen Wert vom Typ

Durch das Hinzufügen dieses Event-Handlers wird jetzt die update-Funktion mit dieser Nachricht aufgerufen, wenn sich der Text im Feld verändert.

 

update

In dieser Funktion kommen alle Nachrichten zusammen, egal aus welcher Quelle sie stammen (Benutzereingabe, Subscriptions, Tasks) und werden ausschliesslich hier behandelt. Natürlich sind Hilfsfunktionen angebracht, nein, erwünscht, aber oft sind diese Updates gar nicht so involviert.

update : Msg -> Model -> ( Model, Cmd Msg ) 
update msg model =
    case msg of
        NoOp ->
            ( model, Cmd.none )
 
        OnInputChanged val ->
            ( { model | input = val }, Cmd.none )

Die Funktion erhält die Nachricht, sowie das aktuelle Modell und retourniert, wie die init-Methode, das neue Modell, sowie allfällige Effekt-Aufforderungen zu diesem Zeitpunkt. Hier sehen wir ein Beispiel für ein «pattern matching». Ein case-statement muss sämtliche möglichen Belegungen des Typs behandeln, was im obigen Beispiel explizit getan wird. (_  ist ein (mit Vorsicht einzusetzender) «catch-all»-Platzhalter, welcher alle verbleibenden Fälle abdeckt)

Wenn eine NoOp-Nachricht reinkommt, tun wir gar nichts mit dem Modell und geben es ohne Effekte zurück. Im zweiten Fall, unserer OnInputChanged-Nachricht, erhalten wir einen Wert val, welches der neue Wert des Textfeldes ist, aus dem die Nachricht stammt.

Der Syntax zum Setzen eines Feldes in einem Record ist etwas speziell, d.h. nach Erläuterung macht die Schreibweise Sinn:

{ model | input = val }

Dieser Syntax bedeutet, dass ein neues Modell erzeugt wird, welches sich nur im Feld input vom alten unterscheidet. Neu hat das Feld den Wert des Nachrichten-Parameters val. Nach diesen Änderungen funktioniert das Textfeld wie erwartet und unser Etappenziel ist erreicht!

 

loop

Das Resultate-Feld ist jetzt halt leer geblieben. Wer möchte, kann im init  Werte einfüllen, und mit Listen von Werten herumspielen, oder selbst versuchen, die Liste interaktive mit Werten zu befüllen. Wir holen uns beim nächsten Mal die Werte von einem Webserver via Http-Request.

Nun aber weiterhin viel Spass beim Ausprobieren!

Elm sitzt mit gekühltem Getränk im Schatten eines grell-gelben Sonnenschirms auf der Terasse des Berggasthauses Bischofalp und fragt sich, wie lange es wohl dauern wird, bis die anderen auch hier eintreffen. Vermutlich haben sich die anderen vor der Abreise noch mit massenhaft unnötigem Material eingedeckt, bevor sie die kurze Strecke in Angriff nahmen. Sie klaubt ihr Handy aus der Hüfttasche und vertreibt die Zeit mit ein paar Spielen:

elm-package install –yes elm-community/list-extra

import List.Extra as List
import String
   
...
 
     OnInputChanged val ->
            let
                anagrams =
                    String.split "" val
                        |> List.permutations
                        |> List.map String.concat
                        |> List.unique
            in
                ( { model | input = val, anagrams = anagrams }, Cmd.none )

Damit fordert sie die Leistungsgrenzen ihres mobilen Browsers nun doch arg heraus. Und da ein Telefon mit Strom heute den Unterschied zwischen Leben und Tod bedeuten könnte, steckt sie es wieder weg… wo bleiben die anderen bloss?

Sind sie wohl schon aufgebrochen? Oder schon zusammengebrochen? Schaffen Sie den ersten Aufstieg des Elm-Höhenweges? Trink noch einen Kaffe Luz und erfahre im nächsten Teil, wie es React und Angular in der Zwischenzeit ergangen ist.

Kommentare

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht