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.