In diesem Kapitel werden wir eine erste Frontend-Anwendung mit Elm entwickeln.

Modulsystem

Wir haben in den vorherigen Kapiteln bereits gelernt, dass Elm-Programme in Modulen organisiert werden. Dabei enthält eine Datei immer ein Modul. Wenn wir das Modul Html mittels

import Html exposing (..)

importieren, stehen uns alle Definitionen aus dem Modul Html unqualifiziert zur Verfügung. Wenn wir ein Modul importieren, können wir eine Definition immer auch qualifiziert verwenden, das heißt, wir können zum Beispiel Html.text schreiben, um die Funktion text aus dem Modul Html zu verwenden. Eigentlich ist es guter Stil, Definitionen qualifiziert zu verwenden, um explizit anzugeben, wo die Definition herkommt. Im Fall des Moduls Html verzichtet man aber häufig darauf, um Programme übersichtlich zu halten. Bei den Funktionen aus dem Modul Html ist im Kontext einer Frontend-Anwendung bereits aus dem Namen eindeutig, um welche Funktion es sich handelt. Ein Aufruf der Form Html.div wirkt zum Beispiel unnötig verbos, da im Kontext einer HTML-Frontend-Anwendung klar ist, dass die Funktion div eine HTML-Struktur erzeugt. Daher importiert man in Elm-Anwendungen die Definitionen aus dem Modul Html häufig unqualifiziert, also zum Beispiel mittels Html exposing (Html, text). Wir werden die Definitionen aus dem Modul Html und ähnlichen Modulen auch immer unqualifiziert verwenden. Das heißt, wir schreiben text und nicht Html.text. Dagegen verwenden wir alle anderen importierten Definitionen immer qualifiziert. Eine ähnliche Empfehlung wird auch im offiziellen Elm Style Guide gegeben.

Qualify variables. Always prefer qualified names. Set.union is always preferable to union. In large files and in large projects, it becomes very very difficult to figure out where variables came from without this.

Unter https://package.elm-lang.org/packages/elm/core/latest/ finden sich Module, die der Elm-Compiler direkt mitbringt. Diese Module werden von jedem Elm-Modul implizit importiert. Der Compiler fügt im Grunde die folgenden Importe zu jedem Modulkopf hinzu.

import Basics exposing (..)
import List exposing (List, (::))
import Maybe exposing (Maybe(..))
import Result exposing (Result(..))
import String exposing (String)
import Char exposing (Char)
import Tuple

import Debug

import Platform exposing (Program)
import Platform.Cmd as Cmd exposing (Cmd)
import Platform.Sub as Sub exposing (Sub)

Das heißt zum Beispiel, dass alle Definitionen aus dem Modul Basics direkt zur Verfügung stehen und wir sie unqualifiziert verwenden können. Im Modul Basics sind ganz grundlegende Definitionen aufgeführt, wie der Datentyp Int und Operatoren wie +. Aus dem Modul String wird nur der Typ String importiert. Das heißt, den Typ String können wir unqualifiziert verwenden. Wenn wir allerdings eine andere Definition aus dem Modul String verwenden möchten, müssen wir diese Definition qualifiziert nutzen. Zum Beispiel können wir String.length schreiben, um die Funktion zu nutzen, die die Länge einer Zeichenkette liefert. Im Fall von Maybe werden durch die Angabe Maybe(..) auch alle Konstruktoren des Datentyps Maybe importiert. Einer der Konstruktoren des Datentyps Maybe heißt Nothing. Das heißt, statt Maybe.Nothing zu schreiben, können wir die Konstruktoren unqualifiziert nutzen und einfach Nothing schreiben. Das gleiche gilt für das Modul Result, auch hier werden der Typ Result und die Konstruktoren von Result unqualifiziert importiert.

Die Namen von Modulen können aus mehreren Komponenten bestehen, die durch Punkte getrennt werden. Diese Art der Module werden als hierarchische Module bezeichnet. In diesem Fall führt man in einigen Fällen kürzere Namen für diese Module ein. Der Import import Platform.Cmd as Cmd bedeutet, dass das hierarchische Modul Platform.Cmd unter dem Namen Cmd importiert wird. Das heißt, wir können die Definitionen aus dem Modul Platform.Cmd qualifiziert nutzen, müssen vor den Namen der Definition aber nicht den gesamten Modulnamen Platform.Cmd schreiben, sondern können stattdessen nur Cmd davor schreiben.

Elm-Architektur

In diesem Abschnitt wollen wir uns über die Architektur einer Elm-Anwendung unterhalten. Die Elm-Architektur wird auch als Model-View-Update-Architektur (MVU) bezeichnet. Wie der Name der Architektur schon sagt, besteht eine Elm-Anwendung aus den folgenden Bestandteilen.

  • Model: das Modell, der Zustand der Anwendung

  • View: eine Umwandlung des Zustandes in eine HTML-Seite

  • Update: eine Möglichkeit, den Zustand zu aktualisieren

Eine typische Elm-Anwendung hat die folgende Struktur.

module Main exposing (main)

import Browser



-- Model


type alias Model =
    ...


init : Model
init =
    ...



-- View


view : Model -> Html Msg
view model =
    ...



-- Update


type Msg =
    ...


update : Msg -> Model -> Model
update msg model =
    ...



-- Main


main : Program () Model Msg
main =
    Browser.sandbox { init = init, view = view, update = update }

Wir haben einen Typ Model, der den internen Zustand unserer Anwendung repräsentiert. Außerdem haben wir einen Typ Msg, der Interaktionen mit der Anwendung modelliert. Der Typ Model ist häufig ein Typsynonym und Msg ist häufig ein Aufzählungstyp, grundsätzlich kann man für beide aber beliebige Typen verwenden. Die Konstante init gibt an, mit welchem Zustand die Anwendung startet. Die Funktion update nimmt eine Nachricht und einen aktuellen Zustand und liefert einen neuen Zustand. Die Funktion view nimmt einen Zustand und liefert eine HTML-Struktur. Außerdem stellt das Modul Browser eine Funktion sandbox zur Verfügung, deren Details wir erst später diskutieren werden. An dieser Stelle müssen wir nur wissen, dass wir der Funktion die Konstante init und die Funktionen update und view, wie oben angegeben, übergeben müssen. Wir geben hier auch den Typ der Funktion main an, werden ihn aber ebenfalls erst später diskutieren.

Wir wollen uns nun ein sehr einfaches Beispiel für eine Elm-Anwendung ansehen. Wir implementieren einen einfachen Zähler, den Nutzer*innen hoch- und runterzählen können.

module Counter exposing (main)

import Browser
import Html exposing (Html, text)



-- Model


type alias Model =
    Int


init : Model
init =
    0



-- View


view : Model -> Html Msg
view model =
    text (String.fromInt model)



-- Update


type Msg
    = IncreaseCounter
    | DecreaseCounter


update : Msg -> Model -> Model
update msg model =
    case msg of
        IncreaseCounter ->
            model + 1

        DecreaseCounter ->
            model - 1



-- Main


main : Program () Model Msg
main =
    Browser.sandbox { init = init, view = view, update = update }

Da wir einen Zähler implementieren wollen, ist unser Zustand vom Typ Int. Initial hat unser Zustand den Wert 0. Um die Nachrichten darzustellen, die Nutzer*innen auswählen können, definieren wir den Aufzählungstyp Msg. Die Funktion update verarbeitet einen Zustand und eine Nachricht und liefert einen neuen Zustand. Die Funktion view liefert zu einem Zustand die HTML-Seite, die den Zustand repräsentiert.

Unserer Anwendung fehlt ein wichtiger Teil, nämlich die Möglichkeit, dass Nutzer*innen mit der Anwendung interagieren. Zu diesem Zweck müssen wir nur zwei Knöpfe zu unserer Seite hinzufügen, die die Nachrichten IncreaseCounter und DecreaseCounter an die Anwendung schicken.

view : Model -> Html Msg
view model =
    div []
        [ text (String.fromInt model)
        , button [ onClick IncreaseCounter ] [ text "+" ]
        , button [ onClick DecreaseCounter ] [ text "-" ]
        ]

Die Funktion button kommt aus dem Modul Html und erzeugt das HTML-Element button. Wir nutzen hier ein div-Element, um den Zähler und die beiden Knöpfe zusammenzufassen. Wie Funktionen wie div genau funktionieren, werden wir in Kürze diskutieren. Das Modul Html.Events stellt die Funktion onClick zur Verfügung. Wir übergeben der Funktion die Nachricht, die wir bei einem Klick an die Anwendung schicken wollen. Wird der Knopf zum Erhöhen des Zählers verwendet, wird die Funktion update mit der Nachricht IncreaseCounter und dem aktuellen Zustand aufgerufen. Nach der Aktualisierung wird die Funktion view aufgerufen und die entsprechende HTML-Seite angezeigt.

Zum Abschluss dieses Kapitels soll noch kurz eine Möglichkeit vorgestellt werden, mit der man in Elm einfaches Print Debugging machen kann. Das Modul Debug stellt eine Funktion log : String -> a -> a zur Verfügung. Wenn diese Funktion ausgewertet wird, schreibt sie ihr zweites Argument auf die Konsole. Der String im ersten Argument wird dieser Ausgabe vorangestellt. Wir nutzen in unserer einfachen Zähleranwendung zum Beispiel die folgende Definition von update.

update : Msg -> Model -> Model
update msg model =
    case msg of
        IncreaseCounter ->
            Debug.log "Modell" (model + 1)

        DecreaseCounter ->
            model - 1

Wir führen die Anwendung nun aus und schauen uns die Entwicklerkonsole unseres Browsers an. Wenn wir wiederholt auf den Knopf für das Erhöhen des Zählers drücken, erhalten wir die folgende Ausgabe.

Modell: 1
Modell: 2
Modell: 3

Da wir an Debug.log den Wert model + 1 übergeben, wird in der Konsole jeweils der Wert angezeigt, den der Zähler nach der Erhöhung hat. Wenn wir auf den Knopf für das Verringern des Zählers drücken, erhalten wir keine Ausgabe, da der Aufruf von Debug.log nur ausgeführt wird, wenn die Nachricht IncreaseCounter lautet.