Grundlagen
In diesem Kapitel führen wir die Grundlagen der Programmiersprache Elm ein. Dazu gehören Konzepte wie primitive Datentypen, boolesche und arithmetische Ausdrücke und Präzedenzen, die aus anderen Programmiersprachen bekannt sind.
Projekt-Setup
Zur Illustration der Beispiele in diesem Kapitel verwenden wir das Kommando elm repl.
Das Akronym REPL steht für Read Evaluate Print Loop und beschreibt eine textuelle, interaktive Eingabe, in der man einfache Programme eingeben (Read), die Ergebnisse des Programms ausrechnen (Evaluate) und das Ergebnis auf der Konsole ausgeben (Print) kann.
Mit dem Begriff Loop wird dabei ausgedrückt, dass dieser Vorgang wiederholt werden kann.
Wir werden die folgenden Programme immer in eine Datei mit der Endung elm schreiben.
Um die Datei als Modul in der REPL importieren zu können, müssen wir den folgenden Kopf verwenden.
module Main exposing (..)
Die zwei Punkte in den Klammern beschreiben dabei, dass wir alle Definitionen im Modul Main exportieren, also zur Verfügung stellen.
Später werden wir in den Klammern explizit die Definitionen auflisten, die unser Modul nach außen zur Verfügung stellen soll.
Um unser Modul in der REPL nutzen zu können, müssen wir zuerst ein Elm-Projekt anlegen.
Zu diesem Zweck muss der Aufruf elm init ausgeführt werden.
Das Kommando elm init legt unter anderem eine Datei elm.json an, die unsere Anwendung beschreibt.
In der elm.json ist zum Beispiel angegeben, dass es sich um eine Anwendung und keine Bibliothek handelt, dass die Elm-Dateien im Ordner src liegen und welche Pakete unsere Anwendung als Abhängigkeiten nutzt.
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
Der Aufruf elm init installiert Basispakete, die bei der Arbeit mit Elm zur Verfügung stehen.
Das Paket elm/core stellt zum Beispiel grundlegende Datenstrukturen wie Listen und Funktionen darauf zur Verfügung und elm/html stellt Kombinatoren zur Verfügung, um HTML-Strukturen zu erzeugen.
Unter https://package.elm-lang.org kann man die Dokumentationen zu den Elm-Paketen elm/core, elm/html und vielen anderen einsehen.
Wir legen die Datei mit unserem Modul im src-Verzeichnis ab, das elm init erstellt hat.
Wir können dann das Modul in der REPL laden, indem wir import Main exposing (..) eingeben.
Die Punkte bedeuten dabei, dass wir alle Definitionen, die das Modul Main zur Verfügung stellt, importieren wollen.
Später werden wir bei einem Import immer genau angeben, welche Definitionen wir importieren wollen.
Kommentare
Wir nutzen hier und im Folgenden immer elm-format um Elm-Programme zu formatieren, damit unsere Programme immer einheitlich formatiert sind.
Der folgende Ausschnitt demonstriert, wie man in Elm Kommentare schreibt.
-- This is a line comment
{-
This is a block comment
-}
{-| This is a doc comment
-}
string : String
string =
"Hello World!"
Ein Doc-Kommentar ist ein Kommentar, der bei der Generierung einer Dokumentation aus Quellcode in diese Dokumentation übernommen wird.
Diese Art von Kommentar wird der Definition, die danach folgt, in diesem Fall string, zugeordnet.
Wie bereits erwähnt, verwenden wir elm-format, um den Quellcode zu formatieren.
Bei einem Zeilen- und einem Blockkommentar, fügt elm-format eine Leerzeile zwischen Kommentar und Definition hinzu.
Da ein Doc-Kommentar sich auf eine Definition bezieht, fügt elm-format zwischen den Kommentar This is a doc comment und die Definition von string keine Leerzeile ein.
Konstanten
Durch die folgende Angabe kann man in Elm eine Konstante definieren.
secretNumber : Int
secretNumber =
42
Die erste Zeile gibt den Typ der Konstante an, in diesem Fall also ein Integer.
Die zweite und dritte Zeile ordnen der Konstante einen Wert zu.
Unser Code Formatter elm-format sorgt dafür, dass der Wert der Konstante in die nächste Zeile geschrieben wird.
Wir erhalten aber auch ein valides Elm-Programm, wenn wir stattdessen secretNumber = 42 schreiben.
Statt des einfachen Doppelpunktes (:) wird in Haskell der doppelte Doppelpunkt (::) für eine Typangabe verwendet.
Wir werden später neben Konstanten noch Funktionen kennenlernen.
Der Begriff Definition wird als Oberbegriff für Konstanten und Funktionen genutzt.
Wir werden daher die Konstante secretNumber im Folgenden auch häufig als Definition bezeichnen.
Den Teil hinter dem =-Zeichen bezeichnet man als rechte Seite der Definition.
Wie der Name Konstante schon besagt, ist der Wert von secretNumber konstant.
Das heißt, wir können den Wert der Definition secretNumber nicht ändern.
Wenn wir zur obigen Definition von secretNumber die Zeile
secretNumber =
43
zu unserem Modul hinzufügen, erhalten wir einen Fehler.
In einer rein funktionalen Programmiersprache sind Definitionen wie secretNumber im Gegensatz zu imperativen Programmiersprachen unveränderbar (immutable).
Das heißt, sie können nach ihrer Definition nicht überschrieben werden.
Definitionen wie secretNumber sind also lediglich Abkürzungen für komplexere Ausdrücke.
In diesem Fall wird die Definition sogar nicht als Abkürzung verwendet, sondern nur, um dem Wert einen konkreten Namen zu geben und diesen an verschiedenen Stellen verwenden
zu können.
Grunddatentypen
Wir haben den Datentyp Int bereits kennengelernt.
Daneben gibt es noch die folgenden Grunddatentypen.
float : Float
float =
4.567
bool1 : Bool
bool1 =
True
bool2 : Bool
bool2 =
False
char1 : Char
char1 =
'a'
char2 : Char
char2 =
' '
string : String
string =
"Hello World!"
Das heißt, im Unterschied zu JavaScript, unterscheidet Elm zwischen dem Typ Int und dem Typ Float.
Arithmetische Ausdrücke
Wir haben gesagt, dass in einer funktionalen Sprache und damit auch in Elm ein Programm ausgeführt wird, indem der Wert eines Ausdrucks berechnet wird. Dies lässt sich sehr schön mithilfe von arithmetischen und booleschen Ausdrücken illustrieren.
Die folgenden Definitionen zeigen einige Beispiele für arithmetische Ausdrücke.
arith1 : Int
arith1 =
1 + 2
arith2 : Int
arith2 =
19 - 25
arith3 : Float
arith3 =
2.35 * 2.3
arith4 : Float
arith4 =
2.5 / 23.2
Wir können die Ergebnisse dieser arithmetischen Ausdrücke in der REPL berechnen.
Wenn wir zum Beispiel unser Modul in der REPL mittels import Main exposing (..) importieren und anschließend arith1 eingeben, berechnet die REPL das Ergebnis (den Wert) des Ausdrucks 1 + 2.
Die Interaktion mit der REPL sieht dann wie folgt aus.
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> import Main exposing (..)
> arith1
3 : Int
Die Ausgabe besagt, dass die Auswertung der Konstante arith1 als Ergebnis den Wert 3 liefert und das Ergebnis vom Typ Int ist.
Elm erlaubt es nicht, Zahlen unterschiedlicher Art zu kombinieren. So liefert die folgende Definition zum Beispiel einen Fehler.
typeError =
secretNumber + float
Die Konstante secretNumber hat den Typ Int, während die Konstante float den Typ Float hat.
Wir können Zahlen nur mit + addieren, wenn sie den gleichen Typ haben.
Daher müssen wir Zahlen ggf. explizit konvertieren.
Um einmal zu illustrieren, dass der Elm-Compiler vergleichsweise gute Fehlermeldungen liefert, wollen wir uns den Fehler anschauen, den Elm liefert, wenn wir versuchen, zwei Zahlen, die unterschiedliche Typen haben, zu addieren.
-- TYPE MISMATCH -------------------------------------------------- src/Test.elm
I need both sides of (+) to be the exact same type.
Both Int or both Float.
15| secretNumber + float
^^^^^^^^^^^^^^^^^^^^
But I see an Int on the left and a Float on the right.
Use toFloat on the left (or round on the right) to make both sides match!
Note: Read <https://elm-lang.org/0.19.1/implicit-casts> to learn why Elm does
not implicitly convert Ints to Floats.
Wir wollen uns also an den Rat halten und die Funktion toFloat verwenden, um den Wert vom Typ Int in einen Wert vom Typ Float umzuwandeln.
Bisher haben wir nur gesehen, wie binäre Infixoperatoren, wie + und * verwendet werden.
Um eine Funktion, wie toFloat in Elm anzuwenden, schreiben wir den Namen der Funktion, dann ein Leerzeichen und dann das Argument, auf das wir die Funktion anwenden wollen.
Wir schreiben zum Beispiel toFloat secretNumber, um den Wert der Konstante secretNumber in einen Float umzuwandeln.
Dieser Ausdruck wendet die Funktion toFloat auf das Argument secretNumber an.
Im Unterschied zu vielen anderen Programmiersprachen, wie Java, C# oder JavaScript werden in Elm die Argumente einer Funktion/Methode nicht geklammert.
In JavaScript schreibt man zum Beispiel toFloat(secretNumber), um eine Funktion toFloat auf ein Argument secretNumber anzuwenden.
Wir werden im Kapitel Funktionen höherer Ordnung genauer lernen, welchen Hintergrund der Unterschied in der Schreibweise von Funktionsanwendungen hat.
Funktionsanwendungen werden auch als Funktionsapplikationen oder einfach Applikationen bezeichnet.
Um unser konkretes Problem zu lösen und die Zahlen secretNumber und float zu addieren, können wir die folgende Definition nutzen.
Das Ergebnis dieser Addition ist dann wieder vom Typ Float, das heißt, die Variable convert hat den Typ Float.
convert =
toFloat secretNumber + float
Im Unterschied zu anderen Sprachen führt der Operator / nur Divisionen
von Fließkommazahlen durch.
Das heißt, ein Ausdruck der Form
secretNumber / 10 liefert ebenfalls einen Typfehler.
Um zwei ganze
Zahlen zu dividieren, muss der Operator // verwendet werden, der eine
ganzzahlige Division durchführt.
Der Ausdruck toFloat secretNumber + float addiert das Ergebnis der Applikation toFloat secretNumber zum Wert von float.
Um dies expliziter auszudrücken, können wir explizit Klammern setzen und (toFloat secretNumber) + float schreiben.
Wie in anderen Programmiersprachen auch sorgen Präzedenzen dafür, dass wir weniger Klammern nutzen müssen.
In Elm (und Haskell) ist die Präzedenz einer Funktionsanwendung höher als die Präzedenz eines Infixoperators wie +.
Daher brauchen wir in dem Beispiel die Klammern nicht.
Boolesche Ausdrücke
Elm stellt die üblichen booleschen Operatoren für Konjunktion und Disjunktion zur Verfügung.
Die Negation eines booleschen Ausdrucks wird in Elm durch eine Funktion not durchgeführt.
bool3 : Bool
bool3 =
False || True
bool4 : Bool
bool4 =
not (bool1 && True)
Im Beispiel bool4 sehen wir auch gleich eine weitere Besonderheit bei der Funktionsanwendung in Elm.
Während das Argument bei der Anwendung einer Funktion auf ein Argument an sich nicht geklammert wird, müssen wir das Argument aber klammern, wenn es sich selbst um das Ergebnis einer Anwendung handelt.
In diesem Beispiel wollen wir etwa das Ergebnis der Berechnung bool1 && True negieren.
Daher klammern wir den Ausdruck bool1 && True und übergeben so das Ergebnis dieser Berechnung an die Funktion not.
Dabei ist zu beachten, dass der Ausdruck (not bool1) && True anders interpretiert wird als der Ausdruck not (bool1 && True).
Im ersten Fall wird das Ergebnis des Ausdrucks not bool1 als erstes Argument an && übergeben.
Im zweiten Fall wird das Ergebnis des Ausdrucks bool1 && True als Argument an not übergeben.
Neben den booleschen Operatoren gibt es die üblichen Vergleichsoperatoren == und /=, so wie <,
<=, > und >=.
Die Funktion == führt immer einen Wert-Vergleich und keinen Referenz-Vergleich durch.
Das heißt, die Funktion == überprüft, ob die beiden Argumente die gleiche Struktur haben.
Das Konzept eines Referenz-Vergleichs existiert in einer rein funktionalen Sprache wie Elm nicht.
bool5 : Bool
bool5 =
'a' == 'a'
bool6 : Bool
bool6 =
16 /= 3
bool7 : Bool
bool7 =
5 > 3 && 'p' <= 'q'
bool8 : Bool
bool8 =
"Elm" > "C++"
Die Funktionen == und /= stehen für jeden Datentyp zur Verfügung.
Die Funktionen <, <=, > und >= stehen dagegen nur für bestimmte Datentypen zur Verfügung.
Präzedenzen und Assoziativität
Um einen Ausdruck der Form 3 + 4 * 8 nicht klammern zu müssen, definiert Elm – wie andere Programmiersprachen – Präzedenzen (Bindungsstärken) für Operatoren.
Die Präzedenz eines Operators liegt zwischen 0 und 9.
Der Operator + hat zum Beispiel die Präzedenz 6 und * hat die Präzedenz 7.
Da die Präzedenz von * also höher ist als die Präzedenz von + bindet * stärker als + und der Ausdruck 3 + 4 * 8 steht für den Ausdruck 3 + (4 * 8).
Wie auch in anderen Programmiersprachen üblich binden die relationalen Operatoren wie <, <=, >, >=, == und /= stärker als die logischen Operatoren && und ||.
Daher steht der Ausdruck 5 > 3 && 'p' <= 'q' ohne Klammern für den Ausdruck (5 > 3) && ('p' <= 'q').
Die Präzedenz einer Funktion ist 10, das heißt, eine Funktionsanwendung bindet immer stärker als jeder Infixoperator.
Der Ausdruck not True || False steht daher zum Beispiel für (not True) || False und nicht etwa für not (True || False).
Wir werden später noch weitere Beispiele für diese Regel sehen.
Neben der Bindungsstärke wird bei Operatoren noch definiert, ob diese links- oder rechts-assoziativ sind. In Elm (wie in vielen anderen Sprachen) gibt es links- und rechts-assoziative Operatoren. Dies gibt an, wie ein Ausdruck der Form x ∘ y ∘ z interpretiert wird. Falls der Operator ∘ linksassoziativ ist, gilt x ∘ y ∘ z = (x ∘ y) ∘ z, falls er rechts-assoziativ ist, gilt x ∘ y ∘ z = x ∘ (y ∘ z). Das heißt, im Unterschied zur Bindungsstärke wird die Assoziativität genutzt, um auszudrücken, wie ein Ausdruck geklammert ist, wenn er mehrfach den gleichen Operator enthält. Im Kapitel Funktionen höherer Ordnung werden wir sehen, dass für einige Konzepte der Programmiersprache Elm die Assoziativität eine entscheidende Rolle spielt.
Listen
Wir wollen uns an dieser Stelle kurz anschauen, wie man in Elm Listen definiert. Wir werden später genauer lernen, wie Listen definiert werden. An dieser Stelle benötigen wir nur die Fähigkeit, einfache Listen zu konstruieren. Wir benötigen die Konstruktion von Listen bereits an dieser Stelle, da die Konstruktion von HTML-Strukturen in Elm Listen verwendet und wir zu Anfang zur Übung HTML-Strukturen erzeugen wollen.
Elm stellt einen vordefinierten Datentyp für Listen zur Verfügung.
Der Datentyp heißt List und erhält nach einem Leerzeichen den Typ der Elemente in der Liste.
Das heißt, wir nutzen den Typ List Int für eine Liste von Zahlen.
Listen werden in Elm mit eckigen Klammern konstruiert und die Elemente der Liste werden durch Kommata getrennt.
Das heißt, die folgende Definition enthält eine konstante Liste, welche die ersten fünf ganzen Zahlen enthält.
In Haskell nutzt der Listendatentyp eine spezielle Syntax und statt List Int schreiben wir in Haskell [Int] für den Typ von Listen mit ganzen Zahlen.
list1 : List Int
list1 =
[ 1, 2, 3, 4, 5 ]
Eine leere Liste stellt man einfach durch zwei eckige Klammern dar, also als [].
In einer Liste können nicht nur Literale wie 1, 2 und 3 stehen, sondern auch wieder Anwendungen von Funktionen oder Operatoren.
list2 : List Bool
list2 =
[ not False, bool1 && bool2 ]
Wenn wir die Konstante list2 in der REPL auswerten, erhalten wir das folgende Ergebnis.
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> import Main exposing (..)
> list2
[True,False] : List Bool
Das heißt, wenn wir die Liste list2 auswerten, erhalten wir die Liste mit True und False.
Außerdem wird noch angegeben, dass der Ausdruck list2 den Typ List Bool hat.
Neben der Literalsyntax [ expression1, expression2 ], mit der man Listen fester Länge erzeugen kann, gibt es noch die Operatoren :: und ++, um Listen zu erstellen.
Der Infixoperator :: hängt vorne an eine Liste ein zusätzliches Element an.
Das heißt, der Ausdruck 1 :: [ 2, 3 ] liefert die Liste [ 1, 2, 3 ].
Der Operator :: ist rechtsassoziativ.
Das heißt, der Ausdruck 1 :: 2 :: [] steht für den Ausdruck 1 :: (2 :: []).
In Haskell wird statt des doppelten Doppelpunktes :: der einfache Doppelpunkt : für die Konstruktion einer Liste verwendet.
Während der Operator :: ein Element an einer Liste anhängt, hängt der Infixoperator ++ zwei Listen hintereinander.
Das heißt, der Ausdruck [ 1, 2 ] ++ [ 3, 4 ] liefert die Liste [ 1, 2, 3, 4 ].
Der Operator ++ ist ebenfalls rechtsassoziativ.
Das heißt, der Ausdruck [ 1, 2 ] ++ [ 3 ] ++ [ 4 ] steht für den Ausdruck [ 1, 2 ] ++ ([ 3 ] ++ [ 4 ]).
Datenstrukturen werden in einer rein funktionalen Sprache nicht verändert.
Das heißt, die Operatoren :: und ++ liefern neue Listen und verändert nicht die bestehende Liste.