5. Web-Apps mit Elm

5.5 Zufall in Elm: Commands und Subscriptions

Zufall in Elm ist schwierig. Eine Funktion random.random() wie in Python oder Math.random() in Java oder Javascript gibt es nicht, ja darf es nicht geben. Überlegen Sie sich, wie die Aufrufe einer (imaginären) Funktion randomInt : Int -> Int -> Int aussähen:

randomInt 0 9
4 : Int
randomInt 0 9
7

Es würde immer eine zufällige Zahl von 0 bis 9 zurückliefern. Das darf aber nicht sein: ein Funktionsaufruf mit den selben Argumenten muss den selben Wert zurückliefern! In streng funktionalen Sprachen kann es also gar keine Funktion random geben. Wie löst Elm also dieses Problem?

Erinnern Sie sich: ein Objekt vom Typ Msg ist eine Nachricht, die uns die Laufzeitumgebung schickt, um uns z.B. über Aktionen des Nutzers zu informieren (ButtonClicked, InputChanged und so weiter). Commands sind das entgegengesetzte Konzept. Mit Commands können wir (also der Elm-Code) die Umgebung bitten, gewisse Dinge zu tun. Meistens verknüpft mit einer Nachricht, die uns bei Vollendung geschickt werden soll. So geht nun Zufall: wir erzeugen einen Command, der die Umgebung bittet, eine Zufallszahl zu erzeugen, und diese dann als Payload einer Msg zu schicken.

import Random                        
Random.float
<function> : Float -> Float -> Random.Generator Float
randomFloatGenerator = Random.float 0 1
Generator <function> : Random.Generator Float

Der Aufruf Random.float 0 1 liefert einen Float-Generator zurück. Dies ist in etwa eine interner Algorithmus, der beschreibt, wie man ein zufälliges Float erzeugt. Mit Random.generate erzeugen wir jetzt einen Command, also eine Bitte an die Laufzeitumgebung, ein Zufallsobjekt zu erzeugen:

Random.generate                        
<function> : (a -> msg) -> Random.Generator a -> Cmd msg

Versuchen wir, diese Signatur zu verdauen. Das Ergebnis eines Aufrufs von Random.generate ist ein Cmd msg, also ein Command-Objekt. Das zweite ist ein Random.generator a, also ein Algorithmus, der zufällige a-Werte erzeugen kann. In unserem Fall wäre a also Float. Das erste Argument ist das kryptischste: es ist ein a -> msg. Das sind beides Typenvariablen. In unserem Fall also Float -> Msg. Eine Funktion, die ein Float nimmt und eine Msg erzeugt? Klar, denn der Generator weiß, wie er ein Float erzeugen kann, nicht aber, wie daraus eine Msg. In der Praxis ist diese "Funktion" meistens einfach ein Konstruktor, also eine Variante des Typs Msg, der eine Payload trägt.

Eine Beispielanwendung finden Sie in Web06RandomFloat.elm. Die auffälligste Änderung ist, dass update eine neue Signatur braucht:

                    update : Msg -> Model -> ( Model, Cmd Msg )

Ausgabetyp ist nicht mehr Model, sondern ein Paar ( Model, Cmd Msg) . Ja klar: die Funktion soll ja nicht nur beschreiben, wie sich das Model bei Eintreffen einer Nachricht ändert, sondern auch, welchen Command es erzeugen soll (wenn überhaupt). In der Praxis wird es bis in wenigen wichtigen Fällen der "leere Command" Cmd.none sein. Auch die Signatur von init ändert sich, weil unter Umständen auch ganz am nfnag ein Command nach draußen geschickt werden soll.

Kompliziertere Zufallselemente

Wir haben nun im Beispiel Web06RandomFloat.elm gesehen, wie wir mittels Random.float 0 1 einen Zufallszahlengenerator erstellen und dann mit Random.generate einen Command erzeugen, mit dem wir die Laufzeitumgebung bitten, jenen Zufallszahlengenerator auch auszuführen und uns das Ergebnis zu senden. Das Modul Random stellt verschiedene Generatoren bereit:

Random.float 0 1 -- zufälliges Float zwischen 0 und 1                        
Random.int 1 6 -- zufällige Zahl in [1,2,3,4,5,6]
Random.uniform "bla" ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] -- zufälliges Element aus der Liste                        
...

Wie erzeugen wir nun aber komplexere Daten, z.B. (Int, Int) von Floats? Natürlich könnten wir zwei Commands an die Laufzeitumgebung senden. Das wäre aber aufwendig und ganz schlechter Stil. Um z.B. ein zufälliges Würfelpaar (x,y) zu erzeugen, könnten wir per Random.int 1 36 und einem Command eine Zufallszahl in der Menge \(\{1,\dots,36\}\) erzeugen und dann mit ein bisschen Arithmetik zwei Zahlen zwischen 1 und 6 erzeugen. Ein (Float, Float) zu erstellen wird schon schwieriger. Ganz zu schweigen von Listen. Hier kommt Random.map2 zu Hilfe:

floatGenerator = Random.float 0 1                        
diceGenerator = Random.int 1 6
Random.map2 (\x y -> (x,y)) floatGenerator diceGenerator
Generator <function> : Random.Generator ( Float, Int )

Wir erhalten also einen Generator für Werte vom Typ ( Float, Int ). Ganz allgemein hat Random.map2 die Signatur:

Random.map2
<function> : (a -> b -> c) -> Random.Generator a -> Random.Generator b -> Random.Generator c
                    

Die Semantik von Random.map2 f gen1 gen2 ist: die Laufzeitumgebung erzeugt einen zufälliges a-Wert mit gen1 und einen b-Wert mit gen2. Die Ergebnisse werden dann in die Funktion f gesteckt, die es zu einem c-Wert weiterverarbeitet.

Einen Generator für Listen von Zufallszahlen erhalten wir so:

Random.list 9 floatGenerator -- Generator für Listen der Länge 9

Übungsaufgabe Kombinieren Sie Knöpfe, Svg-Grafiken und Zufall: auf Knopfdruck soll ein neuer Kreis an einer zufälligen Stelle erzeugt werden.

Übungsaufgabe Erweitern Sie Ihre Lösung der vorherigen Übung. Jetzt soll auch die Farbe des Kreises zufällig sein, also zufällig aus einer fest vorgegebenen Liste ausgewählt, z.B. ["red", "blue", "green", "black", "yellow", "purple", "turquoise"].

Subscriptions

Subscriptions, also in etwa Abonnements, sind so ähnlich wie Commands. Der genaue Unterschied ist im Moment mir auch nicht ganz klar (oder zumindest ist mir nicht klar, warum das in Elm zwei verschiedene Dinge sein müssen). Was wir hier brauchen, ist ein "Uhrticken". Wir weisen also die Laufzeitumgebung an, uns alle \(n\) Millisekunden eine bestimmte Nachricht zu schicken. Konkretes Beispiel ist das Programm Web08Timer.elm. Hier berechnen wir jede Sekunde eine neue Fibonacci-Zahl. Die entscheidenden Zeilen sind:

subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.batch
        [ Time.every 1000 ClockTick
        ]

Hier abonnieren wir einen Uhrtick für jede Sekunde. Uns soll dann die Nachricht ClockTick geschickt werden.

Übungsaufgabe Kombinieren Sie Subscriptions und Svg-Grafiken zu einer kleinen App, in der ein Kreis in eine bestimmte Richtung "fliegt". Als Bonus können Sie einbauen, dass der Kreis an der Bande abprallt und dann gespiegelt zurückfliegt, also das Svg-Feld nie verlässt.

Übungsaufgabe Kombinieren Sie nun Subscriptions, Svg-Grafiken und Mouse-Events zu einer Load and Shoot App, wo man einen Bogen aufziehen und abschießen kann, so wie hier: loadAndShoot.html.