Der folgende Podcast kann genutzt werden, um die Inhalte dieses Kapitels noch einmal zu wiederholen.

Podcast runterladen

Nachdem wir uns die Grundlagen erarbeitet haben, wollen wir ein paar Aspekte der Implementierung der Elm-Architektur näher betrachten. Zuerst einmal wollen wir den Typ der Funktion sandbox diskutieren, die wir verwendet haben, um eine Elm-Anwendung zu erstellen. Die Funktion hat die folgende Signatur.

sandbox :
    { init : model
    , view : model -> Html msg
    , update : msg -> model -> model
    }
    -> Program () model msg

Zuerst können wir feststellen, dass die Funktion einen Record als Argument erhält. Wir haben die Idee, einen Record an eine Funktion zu übergeben, um die Argumente zu benennen, bereits im Abschnitt Records kennengelernt. Der Record, der an sandbox übergeben wird, hat drei Einträge, die init, view und update heißen. Die Funktion ist polymorph über zwei Typvariablen, nämlich model und msg. Der Eintrag init ist vom Typ model. Daher können wir die Funktion sandbox nicht nur mit einem festen Typ verwenden, sondern die Typen für das Modell und die Nachrichten wählen. Wir müssen dabei nur beachten, dass wir alle Vorkommen einer Typvariable durch den gleichen Typ ersetzen.

Die Typen der Einträge view und update unterscheiden sich von den Typen, die wir bisher in Records verwendet haben, da es sich um Funktionstypen handelt. Im Kapitel Funktionale Abstraktionen haben wir bereits gesehen, dass wir in der Programmiersprache Elm Funktionen als Argumente übergeben können. In einer Sprache, in der Funktionen First-class Citizens sind, können wir Funktionen aber nicht nur als Argument übergeben, wir können sie auch in Datenstrukturen ablegen. Daher kann ein Record auch Funktionen enthalten, wie es im Argument von sandbox der Fall ist. Das heißt, sandbox ist eine Funktion höherer Ordnung, die einen Record erhält. Der Record enthält einen Wert und zwei Funktionen.

Das Ergebnis der Funktion sandbox ist ein dreistelliger Typkonstruktor. Dieser erhält den Typ des Modells und den Typ der Nachrichten als Argumente.

Wichtig

Der Typ () wird als Unit bezeichnet und ist der Typ der nullstelligen Tupel. Dieser Typ hat nur einen nullstelligen Konstruktor, nämlich (). Der Unit-Typ wird ähnlich verwendet wie der Typ void in Java.

Das erste Argument von Program wird genutzt, wenn eine Anwendung mit Flags gestartet werden soll. In diesem Fall können der JavaScript-Anwendung, die aus dem Elm-Code erzeugt wird, initial Informationen übergeben werden. Das erste Argument von Program gibt den Typ dieser initialen Informationen an. Da diese Funktionalität bei einer einfachen Elm-Anwendung nicht benötigt wird, wird dem Typkonstruktor Program der Typ () übergeben. Das heißt, die Anwendung erhält beim Start ein Flag, das den Typ () hat. Die Anwendung erhält initial dann einfach immer den Wert (), der aber keinerlei Information enthält.

An der Typsignatur von sandbox erkennt man auch, dass Html ein Typkonstruktor ist. Man übergibt an den Typkonstruktor den Typ der Nachrichten. Das heißt, wenn wir eine HTML-Struktur bauen, wissen wir, welchen Typ die Nachrichten haben, die in der Struktur verwendet werden. Hierdurch können wir dafür sorgen, dass zum Beispiel in den onClick-Handlern der Struktur nur Werte des Typs msg verwendet werden. Wir betrachten etwa das folgende Beispiel.

module Counter exposing (main)

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


type alias Model =
    Int


init : Model
init =
    0


type Msg
    = IncreaseCounter
    | DecreaseCounter


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

        DecreaseCounter ->
            model - 1


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


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

Dieses Beispiel ist eine leichte Abwandlung unseres initialen Beispiels aus dem Kapitel Eine erste Elm-Anwendung. Dieses Programm kompiliert nicht, da die Funktion view eine HTML-Struktur vom Typ Html Msg erstellt, die onClick-Handler, die verwendet werden, aber Nachrichten vom Typ Bool und vom Typ Int versenden.

Um das Beispiel besser zu verstehen, werfen wir einen Blick auf die Signaturen der Funktionen div und onClick.

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

onClick : msg -> Attribute msg

Wir sehen, dass der Typ der Attribute ebenfalls ein Typkonstruktor ist, der den Typ der Nachrichten als Argument erhält. Durch den Typ der Funktion div wird sichergestellt, dass die Attribute den gleichen Nachrichtentyp verwenden wie die Kinder des div. Der Typ der Funktion onClick nimmt eine Nachricht und erzeugt ein Attribut, das Nachrichten vom gleichen Typ enthält.

Bei vielen Attributen und vielen HTML-Elementen spielt der Typ der Nachrichten keine Rolle. Wir betrachten zum Beispiel die Signatur des Attributs style.

style : String -> String -> Attribute msg

Diese Funktion ist polymorph über der Typvariable msg und die Variable wird nur ein einziges Mal verwendet. Daher kann man mit einem Aufruf der Funktion style ein Attribut mit einem beliebigen Nachrichtentyp erzeugen. Auf diese Weise ist es möglich, die style-Funktion für HTML-Strukturen mit beliebigen Nachrichtentypen zu verwenden. Das heißt, Funktionen, für die der Typ der Nachrichten irrelevant ist, verwenden die Typvariable msg kein zweites Mal in ihrer Typsignatur.

Durch diese Modellierung kann gewährleistet werden, dass der Typ der Nachrichten, die an die Anwendung mithilfe von onClick-Handlern geschickt werden, auch von der Funktion update verarbeitet werden kann. Das heißt, mithilfe des statischen Typsystems sorgen wir dafür, dass klar ist, welche Nachrichten unsere update-Funktion verarbeiten können muss. Da wir in Elm in einem case-Ausdruck immer alle möglichen Werte eines Typs verarbeiten müssen, kann es somit nie vorkommen, dass in der Funktion update ein Laufzeitfehler auftritt, da eine Nachricht an die Funktion gesendet wird, die diese nicht verarbeiten kann.

Dadurch, dass der Typ der Nachrichten im HTML-Typ kodiert sind, kann es natürlich vorkommen, dass wir zwei HTML-Strukturen nicht kombinieren können. Wir betrachten zum Beispiel folgendes Beispiel.

type alias Model =
    Int


type Msg
    = IncreaseCounter
    | DecreaseCounter


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


viewButtons : Html Msg
viewButtons =
    div []
        [ button [ onClick IncreaseCounter ] [ text "+" ]
        , button [ onClick DecreaseCounter ] [ text "-" ]
        ]


view : Model -> Html Msg
view model =
    div []
        [ viewText model
        , viewButtons
        ]

Dieses Programm kompiliert nicht, da wir versuchen eine HTML-Struktur vom Typ Html () mit einer HTML-Struktur vom Typ Html Msg zu kombinieren.

Wichtig

Um solche Fälle zu vermeiden, sollten wir einer HTML-Struktur, die gar keine Nachrichten versendet immer einen polymorphen Typ geben.

Das heißt, wir sollten die folgende Definition verwenden.

viewText : Model -> Html msg
viewText model =
    text (String.fromInt model)

Es war im oberen Beispiel unnötig, den Nachrichtentyp auf () einzuschränken, da gar keine Nachrichten verschickt wurden.