Funktionen
In diesem Abschnitt wollen wir uns anschauen, wie man in Elm einfache Funktionen definiert. Funktionen sind in einer funktionalen Sprache das Gegenstück zu (statischen) Methoden in einer objektorientierten Programmiersprache. Bevor wir uns die Definition von Funktionen anschauen, wiederholen und vertiefen wir, wie Funktionen angewendet werden.
Anwendung
Wir haben bei den booleschen Ausdrücken die Funktion not kennengelernt und bereits gesehen, wie die Funktion auf ein Argument angewendet wird.
Ein Elm-Projekt hat standardmäßig eine Reihe von Paketen als Abhängigkeiten.
Das Paket elm/core stellt zum Beispiel grundlegende Datenstrukturen und Funktionen darauf zur Verfügung.
Im Paket elm/core sind zum Beispiel der Datentyp Bool und Funktionen wie not und || definiert.
In der Dokumentation der Funktion not steht neben der Beschreibung der Funktion die folgende Zeile.
not : Bool -> Bool
Diese Zeile besagt, dass not den Typ Bool -> Bool besitzt.
Dies wiederum besagt, dass die Funktion not einen Bool als Argument erhält und einen Bool als Ergebnis liefert.
Daher können wir zum Beispiel not (True && False) schreiben.
Die Auswertung des Ausdrucks True && False liefert einen Wert vom Typ Bool und dieser Wert wird an die Funktion not übergeben.
In der Dokumentation der Funktion xor steht neben der Beschreibung der Funktion die folgende Zeile.
xor : Bool -> Bool -> Bool
Dabei sieht der Typ der Funktion auf den ersten Blick etwas ungewöhnlich aus.
Wir werden später sehen, was es mit diesem Typ auf sich hat.
An dieser Stelle wollen wir nur festhalten, dass die Typen der Parameter bei mehrstelligen Funktionen durch Pfeile getrennt werden.
Das heißt, wenn wir den Typ einer Funktion angeben, listen wir die Typen der Argumente und den Ergebnistyp auf und schreiben jeweils -> dazwischen.
Um die Funktion xor anzuwenden, schreiben wir die Argumente durch Leerzeichen getrennt hinter den Namen der Funktion.
Das heißt, der folgende Ausdruck wendet die Funktion xor auf die Argumente False und True an.
xor False True
Wenn eines der Argumente der Funktion xor das Ergebnis einer anderen Funktion sein soll, so muss diese Funktionsanwendung mit Klammern umschlossen werden.
So wendet der folgende Ausdruck die Funktion xor auf die Argumente False und das Ergebnis der Auswertung des Ausdrucks not True an.
xor False (not True)
Diese Schreibweise stellt für Nutzer*innen, die Programmiersprachen wie Java gewöhnt sind, häufig eine Hürde dar.
Anhand der Klammern und der Leerzeichen kann man zählen, wie viele Argumente einer Funktion übergeben werden.
Diese Anzahl kann man dann mit der Anzahl der Parameter der Funktion vergleichen.
Wir betrachten zum Beispiel die Anwendung xor False not True.
Nach der Leerzeichen- und Klammerregel erhält die Funktion xor hier vier Argumente, nämlich False, not und True, denn diese Argumente sind alle durch Leerzeichen getrennt und keines der Argumente ist von Klammern umschlossen.
Die Funktion xor soll aber nur zwei Argumente erhalten, daher fehlen an dieser Stelle Klammern.
Wenn wir dagegen die Anwendung xor False (not True) betrachten, dann werden zwei Argumente an xor übergeben, denn hinter False folgt zwar ein Leerzeichen, das zweite Leerzeichen ist aber von Klammern umschlossen.
Definition
Eine Zeile wie
not : Bool -> Bool
wird als Typsignatur bezeichnet. Die Typsignatur gibt den Typ einer Funktion oder Konstante an. Die Programmiersprache Elm bietet eine vollständige Typinferenz. Das heißt, der Compiler ist in der Lage alle Typen, die in einem Elm-Programm vorkommen ohen zusätzliche Informationen zu bestimmen. Daher können wir die Typsignatur bei einer Definition in Elm auch weglassen. Es ist aber gute Praxis zu Dokumentationszwecken für Definitionen immer eine Typsignatur anzugeben.
Wir betrachten die folgende einfache Funktionsdefinition in Elm.
Bei einer Definition bezeichnet man den Teil hinter dem =-Zeichen als rechte Seite der Definition.
items : Int -> String
items quantity =
String.fromInt quantity ++ " Gegenstände"
Die erste Zeile bezeichnet man als Signatur und gibt den Namen der Funktion und den Typ der Funktion an.
Der Typ sagt aus, dass die Funktion items ein Argument vom Typ Int nimmt und ein Ergebnis vom Typ String liefert.
Zwischen den Typ des Arguments und den Typ des Ergebnisses schreiben wir in Elm einen Pfeil.
Die zweite und dritte Zeile geben die Definition der Funktion items an.
Die Funktion erhält einen Parameter, der quantity heißt.
Mithilfe der Funktion String.fromInt wird der Wert in der Variable quantity in einen String umgewandelt.
Mit dem Operator ++ wird an diesen String die Zeichenkette " Gegenstände" gehängt.
In diesem Beispiel greift wieder die Regel, dass Funktionsanwendungen – auch Funktionsapplikationen oder nur Applikationen genannt – stärker binden als Infixoperatoren. Daher steht der Ausdruck
String.fromInt quantity ++ " Gegenstände"
für den folgenden Ausdruck.
(String.fromInt quantity) ++ " Gegenstände"
Das heißt, wir hängen den String " Gegenstände" hinter das Ergebnis der Applikation String.fromInt quantity.
Die Funktion fromInt ist im Modul String definiert.
Ein Modul ist vergleichbar mit einer Klasse mit statischen Methoden in einer objektorientierten Programmiersprache.
Wenn wir beim Import nur import String schreiben, ohne ein exposing (..) anzugeben, dann können wir Definitionen aus dem Modul String nur qualifiziert verwenden.
Das heißt, wir müssen vor die Definition, die wir verwenden wollen, noch den Namen des Moduls und einen Punkt schreiben.
Wenn wir statt String.fromInt bei der Anwendung nur fromInt schreiben, nennt man den Namen der Funktion unqualifiziert.
Durch einen qualifizierten Namen können wir direkt am Namen sehen, in welchem Modul die Funktion definiert ist.
Außerdem nutzen wir auf diese Weise den Namen des Moduls als Bestandteil
des Funktionsnamens und können den Namen der Funktion so kürzer fassen.
So kann es zum Beispiel mehrere Funktionen geben, die fromInt heißen
und in verschiedenen Modulen definiert sind.
Durch den qualifizierten Namen ist dann uns (und dem Compiler) klar, welche Funktion gemeint ist.
items : Int -> String
items quantity =
String.fromInt quantity ++ " Gegenstände"
Wir können auch mehrstellige Funktionen definieren.
Die folgende Funktion erhält einen Int und einen String und erzeugt aus beiden Informationen einen einzelnen String.
amount : Int -> String -> String
amount quantity name =
String.fromInt quantity ++ " " ++ name
Um eine mehrstellige Funktion zu definieren, werden die Parameter der Funktion einfach durch Leerzeichen getrennt aufgelistet.
Konditional
Elm stellt einen if-Ausdruck der Form if b then e1 else e2 zur Verfügung.
Im Unterschied zu einer if-Anweisung, wie sie in objektorientierten Programmiersprachen zum Einsatz kommt, kann man bei einem if-Ausdruck den else-Zweig nicht weglassen.
Beide Zweige des if-Ausdrucks müssen einen Wert liefern.
Elm ist eine statisch getypte Programmiersprache.
Das heißt, die Typprüfung wird zur Kompilierzeit durchgeführt.
In statisch getypten Sprachen müssen beide Zweige eines if-Ausdrucks Werte liefern, die den gleichen Typ besitzen.
Das heißt, die Ausdrücke e1 und e2 müssen nach der Auswertung Werte vom gleichen Typ liefern.
Um den if-Ausdruck zu illustrieren, wollen wir die oben definierte Funktion items verbessern.
Die Funktion erhält eine Zahl und liefert eine Pluralisierung des Wortes Gegenstand.
Die Zahl gibt dabei an, um wie viele Gegenstände es sich handelt.
items : Int -> String
items quantity =
if quantity == 1 then
"1 Gegenstand"
else
String.fromInt quantity ++ " Gegenstände"
Um komplexere Programme zu konstruieren, folgt man in Elm — wie in allen Programmiersprachen — Bauprinzipien.
Zum Beispiel können im then- und im else-Zweig eines if-Ausdrucks wieder Ausdrücke stehen.
Da ein if-Ausdruck selbst ein Ausdruck ist, können wir auf diese Weise Mehrfachfallunterscheidungen umsetzen.
Wir betrachten zum Beispiel die folgende Variante der Funktion items.
Hier ist der Ausdruck, der hinter dem Schlüsselwort else steht wieder ein if-Ausdruck.
items : Int -> String
items quantity =
if quantity == 0 then
"Keine Gegenstände"
else if quantity == 1 then
"1 Gegenstand"
else
String.fromInt quantity ++ " Gegenstände"
Zu guter Letzt wollen wir noch die folgende Verallgemeinerung der Funktion items als Beispiel betrachten.
pluralize : String -> String -> Int -> String
pluralize singular plural quantity =
if quantity == 1 then
"1 " ++ singular
else
String.fromInt quantity ++ " " ++ plural
Die Funktion pluralize erhält drei Parameter, die singular, plural und quantity heißen.