In diesem Kapitel wollen wir die Grundlagen für die Definition von Datentypen in Elm einführen. Datentypen entsprechen Objekten in der objektorientierten Programmierung. In einer funktionalen Sprache sind Funktionen aber nicht an einen Datentyp gebunden, wie es bei Methoden der Fall ist.

Typsynonyme

In Elm kann ein neuer Typ eingeführt werden, indem ein neuer Name für einen bereits bestehenden Typ eingeführt wird. Der folgende Code führt zum Beispiel den Namen Width als Synonym für den Typ Int ein. Das heißt, an allen Stellen, an denen wir den Typ Int verwenden können, können wir auch den Typ Width verwenden.

type alias Width =
    Int
Info

In Haskell wird statt der Schlüsselwörter type alias nur das Schlüsselwort type verwendet, um ein Typsynonym zu definieren.

Ein Typsynonym wird verwendet, um einem komplexen Typ einen kürzeren Namen zu geben. Wir werden diesen Effekt sehen, wenn wir Recordtypen kennenlernen.

Wichtig

Ein Typsynonym wie Width ist eigentlich schlechter Programmierstil, da wir ein Typsynonym für einen einfachen Typ einführen.

Bei dieser Modellierung können wir weiterhin jeden Wert vom Typ Int als Width verwenden, auch wenn es sich gar nicht um eine Breite handelt. Wir werden zu Anfang aus didaktischen Gründen diese Form eines Typsynonyms nutzen, später dann aber darauf verzichten. Wir werden später sehen, wie wir diese Fehlnutzung besser verhindern können.

Aufzählungstypen

Wie andere Programmiersprachen stellt Elm Aufzählungstypen (Enumerations) zur Verfügung. So kann man zum Beispiel wie folgt einen Datentyp definieren, der die Richtungstasten der Tastatur modelliert.

type Key
    = Left
    | Right
    | Up
    | Down
Info

In Haskell wird statt des Schlüsselwortes type das Schlüsselwort data verwendet, um einen Aufzählungstyp zu definieren.

Wir können für den Datentyp Key Funktionen mithilfe von Pattern Matching definieren.

Wichtig

Die Werte eines Aufzählungstyps nennt man Konstruktoren.

Das heißt, Left und Up sind zum Beispiel Konstruktoren des Datentyps Key.

Die folgende Funktion verwendet Pattern Matching um zu testen, ob es sich um eine der horizontalen Richtungstasten handelt.

isHorizontal : Key -> Bool
isHorizontal key =
    case key of
        Left ->
            True

        Right ->
            True

        _ ->
            False

Die Teile Left, Right und _ in der Definition von isHorizontal werden Pattern (Muster) genannt. Der Unterstrich ist ein Default-Fall, der für alle Konstruktoren von Key passt. Das heißt, der Fall mit dem Unterstrich (Underscore Pattern) passt für alle möglichen Fälle, die key noch annehmen kann. Im Fall der Funktion isHorizontal wird der Unterstrichfall zum Beispiel verwendet, wenn key den Wert Up oder den Wert Down hat.

Wir können diese Funktion auch definieren, indem wir im Pattern Matching alle Konstruktoren aufzählen und auf den Unterstrich verzichten. Das heißt, die folgende Funktion isHorizontalComplete verhält sich genau so wie die Funktion isHorizontal.

isHorizontalComplete : Key -> Bool
isHorizontalComplete key =
    case key of
        Left ->
            True

        Right ->
            True

        Up ->
            False

        Down ->
            False

Die Verwendung des Unterstrichs ist zwar praktisch, sollte aber mit Bedacht eingesetzt werden. Wenn wir einen weiteren Konstruktor zum Datentyp Key hinzufügen, würde die Funktion isHorizontal zum Beispiel weiterhin funktionieren. Es könnte aber sein, dass das Default-Verhalten für den neu hinzugefügten Konstruktor gar nicht korrekt ist. Bei der Definition isHorizontalComplete erhalten wir vom Elm-Compiler dagegen in diesem Fall einen Fehler, da einer der Fälle nicht abgedeckt ist. Wenn wir immer vollständiges Pattern Matching verwenden, können wir daher nach dem Hinzufügen eines Konstruktors zu einem Datentyp alle Fehlermeldungen des Compilers durchgehen, um das Verhalten für den neu definierten Konstruktor in allen Funktionen zu definieren. Diese Strategie, sich vom Compiler leiten zu lassen, ist in der funktionalen Programmierung verbreitet und wird in der wissenschaftlichen Publikation How Statically-Typed Functional Programmers Write Code als Compilers as Directive Tools bezeichnet.

Wichtig

Man sollte ein Unterstrich-Pattern nur verwenden, wenn man damit viele Fälle abdecken kann und somit den Code stark vereinfacht.

Ein Beispiel ist etwa die Funktion items, die wir mithilfe von Pattern Matching definiert haben. In dieser Funktion müssen wir einen Unterstrich verwenden, da es zu viele mögliche Werte des Typs Int gibt, um sie alle explizit aufzuzählen. Im Fall von isHorizontal sparen wir durch den Unterstrich aber nur eine einzige Regel. In solchen Fällen sollte man auf den Unterstrich verzichten und lieber alle Fälle explizit auflisten.

Als weiteres Beispiel für Pattern Matching betrachten wir einen Datentyp für Monate, der im Elm-Paket elm-time verwendet wird.

type Month
    = Jan
    | Feb
    | Mar
    | Apr
    | May
    | Jun
    | Jul
    | Aug
    | Sep
    | Oct
    | Nov
    | Dec

Wenn wir mit dem Paket elm-time arbeiten, können wir wie folgt eine Funktion definieren, die für einen Monat einen für deutsche Nutzer*innen lesbaren Namen liefert.

monthToString : Month -> String
monthToString month =
    case month of
        Jan ->
            "Januar"

        Feb ->
            "Februar"

        Mar ->
            "März"

        Apr ->
            "April"

        May ->
            "Mai"

        Jun ->
            "Juni"

        Jul ->
            "Juli"

        Aug ->
            "August"

        Sep ->
            "September"

        Oct ->
            "Oktober"

        Nov ->
            "November"

        Dec ->
            "Dezember"