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 dem struct 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. intRangemehr 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.