8. Elm - Eine funktionale Programmiersprache zur Entwicklung von Web-Apps
8.1 Ein Crashkurs in Elm
Funktionen, Signaturen, Rekursion
Schreiben wir nun unsere erste Elm-Funktion sqr
. Sie soll das
Quadrat einer Zahl berechnen. Ähnlich wie in Java oder C müssen wir die
Signatur der Funktion angeben. In Java wäre dies zum Beispiel
public float sqr (float x)
In Elm sieht das so aus:
sqr : float -> float
sqr x =
x * x
Gehen Sie nun mit der Konsole in das Verzeichnis elm
(nicht
elm/src
)
und geben ein
elm repl
sqr 2.5
Das sollte eine Fehlermeldung geben.
Sie müssen das Modul Test
erst noch importieren. Also, noch einmal:
import Test exposing (..)
sqr 2.5
Als Ergebnis sollte nun 6.25 : Float
erscheinen. Sie
sehen:
bei Ausgabewerten gibt Elm immer den Typ des Objekts mit an.
Dinge wie if,then,else
und Rekursion funktionieren ohne größere Überraschungen:
fib : Int -> Int
fib n =
if n == 0 then
0
else if n == 1 then
1
else
fib (n - 1) + fib (n - 2)
Wenn Sie das Fehlen des Schlüsselwortes return
verwirrt,
dann rufen Sie sich ins Gedächtnis, dass Elm eine funktionale Sprache ist.
Es gibt also gar keine Anweisungen / statements, daher ist jede Zeile in einer
Funktion,
in der nichts Anderes (wie z.B. if
oder so) steht, eine "return"-Zeile, und das
Schlüsselwort return
somit redundant.
Gehen Sie nun auf die Konsole, wo noch Ihr elm-repl-Fenster auf ist, und tippen ein
fib 5
Sie müssen nicht noch einmal import Test exposing (..)
eingeben.
Das Repl-Fenster "überwacht" die Datei sozusagen und bekommt jede Änderung mit. Sie können
natürlich in Ihrer Datei auch Konstanten definieren, zum Beispiel
n_0 : Int
n_0 =
25
Primitive und fest eingebaute Datentypen
Es gibt die gleichen fundamentalen Datentypen wie in anderen Programmiersprachen auch:
s = "Hello"
"Hello" : String
'H'
'H' : Char
s == "Hello"
True : Bool
s == "Hallo"
False : Bool
s == 'H'
-- komplexe Fehlermeldung, weil die Typen nicht übereinstimmen
Es gibt also String, Char, Bool
wie in anderen Sprachen auch.
Records / Structs
Wir können Record-Typen anlegen, um Daten zu bündeln, ähnlich demstruct
in C:
type alias Datum =
{ jahr : Int
, monat : Int
, tag : Int
}
type alias Student =
{ name : String
, matrikelNummer : Int
, geburtsDatum : Datum
}
s1 : Student
s1 =
{ name = "Johannes Schneider"
, matrikelNummer = 6251294
, geburtsDatum = { jahr = 2000, monat = 1, tag = 1 }
}
und dann im Repl-Fenster
s1.name
"Johannes Schneider" : String
s1.geburtsDatum
{ jahr = 2000, monat = 1, tag = 1 } : Datum
s1.geburtsDatum.jahr
2000 : Int
Übungsaufgabe
Schreiben Sie einen Record-Typ IntPaar
, das
paar.erstes
und paar.zweites
hat.
Schreiben Sie nun eine "schnelle" rekursive funktion
fibFast : Int -> IntPaar
,
die hohe Fibonacci-Zahlen wie zum Beispiel
\(F_n\) ohne Probleme berechnen kann.
Tip. Sie werden höchstwahrscheinlich eine
Helfer-Funktion nextPair
oder so brauchen, die aus \(a,b\) das Paar
\(b, a+b\) macht.
Zwischenergebnisse mit let / in
Sie können in Elm Zwischenergebnisse berechnen und ihnen eine Wert zuweisen.
In Racket hatten sie hierfür let
; das Schlüsselwort in Elm heißt auch
let
, allerdings ist die Syntax weniger exotisch.
Sie sehen jetzt die Lösung für die obige Übungsaufgabe, allerdings mit let
,
sodass wir keine Helferfunktion nextPair
mehr brauchen.
type alias IntPaar =
{ erstes : Int
, zweites : Int
}
fibFast : Int -> IntPaar
fibFast n =
if n == 0 then
{ erstes = 0, zweites = 1 }
else
let
paar : IntPaar
paar =
fibFast (n - 1)
in
{ erstes = paar.zweites, zweites = paar.erstes + paar.zweites }
In Zeilen 60/61 deklariere/definiere ich eine Zwischenergebnis-Variable paar
.
Nach dem Schlüsselwort in
kommt der return-Wert der Funktion, und in dem
darf ich dann nach herzenslust die zuvor in let
definierten Bezeichner
verwenden.
Datentypen unbürokratisch mit Tuple
erschaffen
Wenn Sie nur schnell mal zwei Werte bündeln wollen, dann müssen Sie nicht extra
einen Datentyp dafür erschaffen; Sie können einfach
paar = (2,3)
schreiben und dann mit
Tuple.first paar
und
Tuple.second paar
auf die beiden
Stellen zugreifen. Das macht den Code von fibFast
einfacher.
ff : Int -> ( Int, Int )
ff n =
if n == 0 then
( 0, 1 )
else
let
paar =
ff (n - 1)
in
( Tuple.second paar, Tuple.first paar + Tuple.second paar )
Listen, Pattern matching, map, filter
To be done.Currying
Elm verwendet für +, *, <
etc. Infix-Notation (im Gegensatz zu Racket).
Wenn Sie aber Präfixnotation wollen, dann kriegen Sie die mit
(+), (*), (<)
. In der Tat:
(+) 3 5
8 : number
(<) 3 5
True : Bool
(<)
<function> : comparable -> comparable -> Bool
Die letzte Zeile sagt Ihnen, dass (<)
eine Funktion ist,
die zwei Objekte vom Type comparable
(also z.B. Int
)
nimmt und Bool
zurückliefert.
Was geschieht, wenn wir (<)
mit "zu wenig" Argumenten aufrufen?
(<) 3
<function> : number -> Bool
f = (<) 3
<function> : number -> Bool
Wenn Sie in Elm eine Funktion mit "zu wenig" Argumenten aufrufen, dann erzeugt dies keine
Fehlermeldung, sondern eine neue Funktion, die geduldig auf die restlichen Argumente
wartet. Die gerade definierte Funktion f
wartet also auf ein
comparable
und, wenn es dies erhält:
f 4
True : Bool
f 5
True : Bool
f 2
False : Bool
Wir haben mit f = (<); 3
also eine Funktion greaterThan3
erschaffen.
Diese Prinzip, Funktionen nur mit einer Teilliste ihrer Argumente aufzurufen, nennt man
Currying.
Jetzt macht auch die Signatur von z.B. intRange
mehr Sinn:
intRange : Int -> Int -> List Int
Expliziter könnten wir schreiben:
intRange : Int -> (Int -> List Int)
Das hieße dann, dass intRange
, mit einem Int
aufgerufen,
eine Funktion zurückgibt, die wiederum ein Int
als Argument nimmt,
und daraus eine Liste bastelt. In der Tat
g = intRange 5
<function> : Int -> List Int
g 10
[5,6,7,8,9,10] : List Int
g 5
[5] : List Int
Wenn Sie List.map
, List.filter
, List.foldl
und
Currying verstehen, dann werden Sie kaum selbst rekursive Funktionen schreiben müssen.
Ein Beispiel, das Currying und List.filter
verwendet, ist
der folgende kleine Code, der aus einer Liste alle Elemente herausfischt, die
größer sind als 3:
List.filter ( (<) 3 ) [6,1,4,2,3,5]
[6,4,5
Übungsaufgabe Implementieren Sie Quicksort in Elm. Zur Erinnerung: hier ist der Pseudocode:
quicksort(list): if list == []: return [] else: pivot = list[0] smaller = those in list that are smaller than pivot equal = those in list that are equal to pivot greater = those in list that are greater than pivot return quicksort(smaller) ++ equal ++ quicksort(greater) // append all three lists
Union-Datentypen
Hier ist ein Teilcode für eine Binary-Search-Tree-Typ.
module BST exposing (..)
type BST
= Empty
| Node Int BST BST
-- Sie können einen Baum nun zum Beispiel per
-- Node 3 (Node 4 Empty Empty) Empty erschaffen.
contained : Int -> BST -> Bool
contained key tree =
case tree of
Empty ->
False
Node keyHere leftTree rightTree ->
if (key == keyHere) then
True
else if key < keyHere then
contained key leftTree
else
contained key rightTree
insert : Int -> BST -> BST
insert key tree =
Übungsaufgabe
Vollenden Sie die Funktion insert
.