Vor einiger Zeit habe ich der Episode We’re Teaching Functional Programming Wrong des Podcasts Corecursive lauschen müssen. Darin erzählt ein Richard Feldman von einer rein funktionalen Sprache mit der man tatsächlich dynamische Web-Seiten erstellen kann, ohne verstehen zu müssen, was ein Monad ist. Da ich objekt-orientierte Programmierung für überbewertet halte*Der Vortrag Free your Functions von Klaus Iglberger beleuchtet einige praktische Aspekte meines Standpunkts., habe ich diesen Ansatz kurze Zeit später ausprobiert. In diesem Post werde ich von meinen ersten Gehversuchen, Hürden und Fortschritten in Elm berichten und den Fokus auf für mich ungewohnte Aspekte legen.

Elm Installation und Entwicklungsumgebung

Man kann z.B. wie ich den Paketmanager npm verwenden oder alternativ Binaries herunterladen. Als IDE verwende ich VS Code. Zusätzlich habe ich elm-format per npm installiert, damit VS Code per Shift-Alt-F meinen Quelltext formatieren kann. Mit

elm make src/File.elm --output app.js

kompiliert man eine Elm-Quelldatei nach JavaScript. Das Erzeugnis kann man per


<script src="app.js"></script>
<script>
    Elm.Calc.init({node: document.getElementById("app")});
</script>

in eine HTML-Seite einbinden.

Private Altersvorsorge

Da private Altersvorsorge bei uns derzeit ein Thema ist, habe ich als Mini-Testprojekt einen Rechner gebaut, der die jährliche Verzinsung, die monatliche Sparrate und die Anzahl der Sparjahre erhält und den Kontostand am Ende der Sparzeit ausspuckt. Das Ergebnis seht ihr in der folgenden Box, die ich hier per <iframe></iframe> eingebunden habe.

Der Fokus liegt im Folgenden weder auf den Formeln noch auf einer grundlegenden Einführung in Elm. Stattdessen präsentiere ich ein paar für imperativ programmierende Wesen*Künstliche Intelligenzen sind mitgemeint wie mich*Ich habe in der Vergangenheit viel mit Python und C++ programmiert. Mit JavaScript habe ich fast keine Erfahrungen. Als Schüler habe ich JavaScript-Schnipsel per Copy+Paste verwendet um Button-Bilder beim Hover-Event der Maus auszutauschen. Und kürzlich habe ich ein kleines Nuxt-Frontend zusammengestümpert. ungewohnte Aspekte. Eine grundlegende Einführung in Elm findet man z.B. im Elm-Guide. Ich habe auch das Buch Elm in Action*Achtung! Das ist KEIN Affiliate-Link! Achtung! käuflich erworben. Meinen kompletten Code gibt es auf Github.

Das letzte Element

Die sequentielle Standarddatenstruktur in Elm ist eine Liste. Im Gegensatz zu Python-Listen gibt es keinen Random-Access. Man kann also nicht per someList[i] auf das i-te Element der Liste zugreifen. Man kann auch nicht auf das letzte Element der Liste zugreifen. Da ich das ungefähr die ganze Zeit benötigt habe, ist die folgende kleine Hilfsfunktion last entstanden.

last : List Float -> Float
last inputs =
    Array.fromList inputs 
        |> Array.get (List.length inputs - 1) 
        |> Maybe.withDefault -1

In den ersten beiden Zeilen sehen wir die Signatur der Funktion. Es wird eine Liste inputs als Argument erwartet. Aus dieser wird eine Fließkommazahl als Ergebnis berechnet. Wie dem imperativ programmierenden Wesen eventuell aufgefallen ist, fehlen sowohl Klammern um Argumente als auch ein return-Schlüsselwort. Beides wird in Elm nicht benötigt. Der Wert des Ausdrucks wird ohne return direkt zurückgegeben. Um nun das letzte Element der Liste zurückzugeben, wandeln wir die Liste in einen Array um, der in Elm Random-Access erlaubt. Man könnte sich fragen, ob man sich die Kopie sparen könnte indem man einfach immer einen Array verwendet. Allerdings sind für den Typen Array in Elm andere hilfreiche Funktionalitäten wie map2, range oder sum nicht implementiert, die ich an anderer Stelle verwendet habe.

Funktionen werden mit |> komponiert, d.h. $(f \circ g)(x)=f(g(x))$ kann in Elm durch

g x |> f

implementiert werden. Eines meiner Lieblingsfeatures in Elm ist, dass partielle Funktionsanwendung direkt verfügbar ist. Wenn man z.B. in Python eine Funktion def g(x, y): [...] definiert hat, kann man durch

f = partial(g, x=2)

eine Funktion f in y definieren, die der Funktion g mit x=2 entspricht. In Elm ist eine zusätzliche Funktion höherer Ordnung wie partial aus dem Python-Modul functools für partielle Funktionsanwendung nicht nötig. Die entsprechende partielle Anwendung der Funktion g x y = [...], also die Definition einer Funktion in y mit festem x=2 ist einfach

g 2.

Das heißt, in der Funktion last wandelt Array.fromList eine Liste in einen Array um und übergibt den Array an die partiell angewandte Funktion

Array.get (List.length inputs - 1).

Da

Array.get: Int -> Array a -> Maybe a

einen Integer und einen Array mit Elementen des Datentypen a als Argumente erwartet und das Element an der Stelle definiert durch den Integer-Parameter zurückgibt, liefert die partiell angewandte Funktion Array.get (List.length inputs - 1) das letzte Element des Arrays. Genau genommen ist das Ergebnis nicht das letzte Element der Liste sondern ein Maybe, also ein optionaler Wert, der existieren kann oder auch nicht. Ich drücke mich in diesem Post um saubere Fehlerbehandlung, z.B. im Falle einer leeren Liste, und packe in Annahme der Existenz des letzten Elements den optionalen Typ einfach mit der partiell angewandten Funktion Maybe.withDefault -1 aus. Diese gibt im Fehlerfall eine -1 zurück.

Testen und Debuggen

Von Elm-programmierenden Wesen*Auch hier schließe ich künstliche Intelligenzen mit ein. Ich habe aber noch keine Elm-programmierende künstliche Intelligenz getroffen. habe ich den Satz “If it compiles, it works” gehört. Das klingt zwar gut, geht aber streng genommen zu weit. Denn Logikfehler im Programm kann der Compiler natürlich nicht erkennen. Der einfachste Weg, um die Richtigkeit der Funktion festzustellen, den ich gefunden habe, besteht in der Verwendung des npm-Pakets elm-test. Mit den Imports

import Expect
import Test exposing (..)

kann man folgenden Test-Code für die Funktion last schreiben.

testLast : Test
testLast =
    describe "last"
        [ test "last with 2 elts"
            (\_ -> Expect.equal (last [ 0, 4 ]) 4)
        , test "last with 1 elts"
            (\_ -> Expect.equal (last [ 4 ]) 4)
        , test "last with 10 elts"
            (\_ -> Expect.equal (last (List.repeat 9 0 ++ [ 4 ])) 4)
        , test "last with 0 elts"
            (\_ -> Expect.equal (last []) -1)
        ]

Hier sehen wir auch ein weiteres typischen Merkmal funktionaler Programmierung und zwar anonyme Lambda-Funktionen. Zur Erklärung der Notation betrachten wir das Beispiel \x -> x * x. Diese Funktion berechnet das Quadrat ihres Arguments x. Die im Test-Code vorkommende Lambda-Funktion

\_ -> Expect.equal (last [ 0, 4 ]) 4)

erwartet keine Argumente und ist ein valider zweiter Parameter der Funktion test. Einen vernünftigen Weg, Elm-Programme interaktiv zu debuggen habe ich noch nicht gefunden. Ich konnte zwar an Breakpoints im JavaScript-Kompilat anhalten. Daraus bin ich aber nicht schlau geworden. Wenn man per Ausgabe und ohne Breakpoints debuggen möchte, kann man Debug.log verwenden. Dabei gibt die Funktion ihr zweites Argument zurück und logt es zusätzlich in die Konsole. Wenn man also Zeile 5 im obigen Snippet durch

(\_-> ;Expect.equal (Debug.lo "last o [0, (last [ 0, 4 ])) 4)

ersetzt, funktioniert der Test weiterhin da das zweite Argument von Debug.log einfach weitergereicht wird. Das erste Argument ist vom Typ String und dient als Beschreibung des Logs. Wenn man den Test ausführt, erhält man die Ausgabe

last of [0, 4]: 4.

Man kann Debug.log auch in Programmteilen verwenden, die kein Test-Code sind.

Konstanten und Variablen

Um eine lokale Konstante in einer Funktion anlegen zu können, kann man die Schlüsselwörter let und in verwenden. Variablen, die nicht konstant sind, gibt es in Elm erwartungsgemäß nicht. Man könnte z.B. die Funktion last mit einer temporären Variable wie folgt umschreiben, was ich hier nur zu rein demonstrativen Zwecken getan habe.

last : List Float -> Float
last inputs =
    let
       inputsArr = Array.fromList inputs
    in
    Array.get (List.length inputs - 1) inputsArr 
        |> Maybe.withDefault -1

Das let-in-Konstrukt lässt sich auch verschachteln und innerhalb des let-Teils lassen sich neben Konstanten auch Funktionen definieren, die nur im in-Teil sichtbar sind, wie wir im nächsten Abschnitt sehen werden.

for-Loops und if-Expressions

In Elm gibt es keine Schleifen. Man verwendet wenn möglich Funktionen wie map, filter oder foldl. Das Analogon zu foldl heißt in Python reduce. Falls der gegebene Sachverhalt sich durch map, filter, foldl oder Ähnliches nicht darstellen lässt, müssen anstatt Schleifen rekursive Funktionen verwendet werden, wie wir im folgenden Beispiel begutachten können.

linearPrices : Float -> Int -> Float -> List Float
linearPrices rate nYears startPrice =
    let
        recurse : Int -> List Float -> List Float
        recurse y prices =
            let
                newPrices =
                    linearPricesOfYear rate (last prices)
            in
            if y < nYears then
                newPrices ++ recurse (y + 1) newPrices
            else
                []
    in
    recurse 0 [ startPrice ]

Die Funktion linearPrices wandelt in einem Zwischenschritt des Rechners einen jährlichen Zinssatz in fiktive Kursstände um, die den Namen prices tragen. Somit können wir den Rechner auch mit leichten Anpassungen auf simulierte oder historische Aktienkurse anwenden. Im let-Block wird eine rekursive Funktion definiert, die in einer imperativen Programmiersprache eher als Schleife geschrieben worden wäre. Die Abbruchbedingung finden wir in Zeile 10, das Erhöhen der Zählvariablen in Zeile 11. Im inneren let-Block wird die Liste newPrices berechnet. Bei Betrachten der Abbruchbedingung könnte das imperativ programmiererende Wesen über die Frage stolpern: “Was gibt die Funktion recurse zurück?” Die Antwort ist, dass if-else-Ausdrücke immer einen Wert zurückgeben genau wie der ternäre ?:-Operator in C++. Des Weiteren und wie bereits erwähnt kennt Elm das Schlüsselwort return nicht. Es wird also beispielsweise bei Erfüllung der Abbruchbedingung die leere Liste [] zurückgegeben.

Typen

Man kann ein sogenanntes Record durch

type alias Model =
    { rate : Float
    , regularPayment : Float
    , nYears : Int
    }

definieren. In Python ist dem vielleicht ein NamedTuple am ähnlichsten, auch wenn die Notation eher an ein dict erinnert. Wir haben dem Record den Namen Model gegeben. Mit type alias gibt man einem existierenden Typen einen neuen Namen. Im Unterschied dazu definieren wir im Folgenden Schnipsel den neuen Typ Msg.

type Msg
    = ChangedRate String
    | ChangedRegPay String
    | ChangedYears String

Dieser Typ erinnert erstmal an einen enum-Typ in Python oder C++, der aber zusätzlich zu seinem Wert, der entweder ChangedRate, ChangedRegPay oder ChangedYears annimmt, noch etwas Weiteres transportieren kann. In diesem Fall ist dieses Weitere für jeden der Werte eine Zeichenkette. Die folgende Funktion demonstriert die Verwendung unseres neuen und unseres neu benamten Typs mit Hilfe des case-Ausdrucks.

update : Msg -> Model -> Model
update msg model =
    case msg of
        ChangedRate newContent ->
            { model
                | rate =
                    String.toFloat newContent
                        |> Maybe.withDefault 0
            }
        ChangedRegPay newContent ->
            { model
                | regularPayment =
                    String.toFloat newContent
                        |> Maybe.withDefault 0
            }
        ChangedYears newContent ->
            { model
                | nYears =
                    String.toInt newContent
                        |> Maybe.withDefault 0
            }

Dem imperativ programmierenden Wesen sei noch gesagt, dass case einen Wert zurückgibt, ohne dass ein return-Schlüsselwort verwendet wird. Im case-Ausdruck der update-Funktion wird der Wert von msg überprüft. Nehmen wir o. B. d. A. an, msg habe den Wert ChangedRate. Dann wird

    case msg of
        ChangedRate newContent ->
            { model
                | rate =
                    String.toFloat newContent
                        |> Maybe.withDefault 0
            }

ausgewertet. ChangedRate newContent -> erlaubt auf den von ChangedRate transportierten String über den Namen newContent zuzugreifen. Der Wert von newContent wird in eine Fließkommazahl umgewandelt und ein Model-Record wird zurückgegeben, bei dem nur der Wert des Felds rate mit dem neuen Wert belegt wird. Der Rest wird vom Argument model kopiert. Das wird durch

{ model | rate = [...] }

bewerkstelligt*Ich benutze oft für genau diesen Zweck in Python eine selbstgeschriebene Funktion, die ein `NamedTuple` kopiert und nur die separat übergebenen Argumente ersetzt. In Elm ist das erfreulicherweise einfach ein Sprach-Feature.. Wir sehen wieder, dass ein Maybe ausgepackt wird. Bei der Konvertierung einer Zeichenkette in eine Fließkommazahl kann natürlich einiges schief gehen. Hier wird im Fehlerfall einfach eine 0 zurückgegeben. Das lässt sich oben im Rechner auch direkt nachvollziehen. Wenn man in eines der Felder einen Buchstaben tippt, wird der Wert auf 0 gesetzt.

Dynamische Web-Applikationen

Jetzt habe ich die ganze Zeit über einzelne Elm-Konstrukte geredet, die für mich als imperativen Programmierer ungewohnt sind. In diesem Abschnitt möchte ich noch kurz darauf eingehen, wie mit Elm dynamische Web-Frontends gestaltet werden können. Für genauere, bessere und umfangreichere Darstellungen verweise ich wieder auf den Elm-Guide.

Kenntnisse in HTML sind im Folgenden hilfreich*Dass meine HTML-Kenntnisse, die ich mir als Schüler angeeignet habe, 25 Jahre später immer noch relevant sind, amüsiert mich.. Zum Rendern des Frontends werden eine Funktion view und eine Funktion update benötigt, die uns im letzten Abschnitt bereits begegnet ist. Die view-Funktion, die unseren unseren Rechner von oben rendert, sieht wie folgt aus.

 Html Msg
view model =
    div []
        [ label [ for "rate" ] [ text "Interest rate in %" ]
        , br [] []
        , input
            [ id "rate"
            , value (String.fromFloat model.rate)
            , onInput ChangedRate
            ]
            []
        , br [] []
        , label [ for "regpay" ] [ text "Payment each month" ]
        , br [] []
        , input
            [ id "regpay"
            , value (String.fromFloat model.regularPayment)
            , onInput ChangedRegPay
            ]
            []
        , br [] []
        , label [ for "years" ] [ text "Years" ]
        , br [] []
        , input
            [ id "years"
            , value (String.fromInt model.nYears)
            , onInput ChangedYears
            ]
            []
        , br [] []
        , div [ style "font-weight" "bold" ]
            [ text
                (String.fromFloat
                    ((finalBalance (1 + (model.rate / 100))
                        model.regularPayment
                        model.nYears
                        * 100
                        |> round
                        |> toFloat
                     )
                        / 100
                    )
                )
            ]
        ]

Die HTML-Funktionen wie div, label, input und br definieren das Aussehen und werden aus dem Html-Module importiert*Und einmal mehr sind Elm und ich uns einig. Denn das Elm-Modul heißt `Html` und nicht `HTML` obwohl es sich um eine Abkürzung handelt. Bei Camel-Case bevorzuge ich diese Schreibweise wegen der besseren Lesbarkeit von `HtmlButton` gegenüber `HTMLButton`.. Sie erwarten jeweils zwei Argumente und zwar eine Liste von Attributen und eine Liste von Kindern. In unserer view-Funktion wird also das Layout definiert und ein variabler Teil der Web-Seite wird durch das Argument model gefüllt. Der Rückgabewert der view-Funktion ist vom Typ Html Msg. Dabei ist Msg eine sogenannte Typvariable, vergleichbar mit Templates in C++ oder Generics in der Programmiersprache, deren Name nicht genannt werden darf*Sie fängt mit J and und hört weder mit avaScript noch mit ulia auf. Gruß an Nathan.. Also Msg ist selbst auch ein Typ. Der Inhalt der Msg Instanz wird durch den ausgelösten Event-Handler in einem der Input-Feldern definiert. Das heißt, wenn es o. B. d. A. eine Änderung im rate-Feld gibt, wird onInput ChangedRate ausgelöst und die Msg-Instanz mit dem Wert ChangedRate an die update-Funktion geschickt. Zusätzlich transportiert die Msg-Instanz den Wert des value-Attributes. Die update-Funktion passt entsprechend der Änderung in msg das Argument model an wie oben in der Definition gesehen. Und das Modell wird dann wieder an die view-Funktion zur Anzeige des aktualisierten Zustands geschickt. Damit die Benutzerschnittstelle gerendert wird, muss man in einer main-Funktion die update- und die view-Funktion registrieren.

main =
    Browser.sandbox { init = init, update = update, view = view }

Hinter dem init-Parameter verbirgt sich ein intiales Model. Fertig.