In einer rein funktionalen Sprache wie Elm muss eine Funktion, die mit denselben Argumenten aufgerufen wird, immer denselben Wert zurückgeben.
Deshalb lässt sich eine Funktion mit der Typsignatur rand : Int nur als rand = 4 implementieren, was keine gute Grundlage für das Erzeugen zufälliger Ganzzahlen ist.
Wie erzeugen wir also zufällige Werte in Elm?
Wir teilen das Problem in zwei Teile: Zuerst beschreiben wir den Wert, den wir erzeugen wollen, mit einem Random.Generator a, dann erzeugen wir einen Wert.
Die erste Möglichkeit, einen Wert zu erzeugen, besteht darin, über initialSeed : Int -> Seed einen Random.Seed zu erstellen und dann step : Generator a -> Seed -> ( a, Seed ) zu verwenden, was einen Wert und einen neuen Seed zurückgibt.
Beachte, dass beide Funktionen rein sind: Rufst du sie zweimal mit denselben Argumenten auf, erzeugen sie dieselben Werte.
Die zweite Möglichkeit, einen Wert zu erzeugen, ist generate : (a -> msg) -> Generator a -> Cmd msg, aber das funktioniert nur innerhalb einer Elm-Anwendung.
In diesem Fall kann die Elm-Laufzeit sowohl step als auch äußere, nicht-reine Ressourcen nutzen, um Seeds zu erzeugen.
Ab jetzt konzentrieren wir uns auf Generatoren.
Stellen wir uns, um Beispiele zeigen zu können, vor, wir hätten mit initialSeed und step eine Funktion generate : Int -> Generator a -> List a definiert, die eine Anzahl von Werten aus einem Generator erzeugt.
Das Random-Modul bietet zwei einfache Generatoren, um Ganzzahlen und Gleitkommazahlen innerhalb eines bestimmten Bereichs zu erzeugen:
generate 5 (Random.int -5 5)
--> [0, 3, -5, 5, 0]
generate 3 (Random.float 0 5)
--> [1.61803, 3.14159, 2.71828]
Diese Werte lassen sich zu Tupeln oder zu Listen von Werten kombinieren:
generate 2 (Random.list 3 (Random.int 0 3))
--> [[0, 3, 3], [1, 3, 2]]
generate 2 (Random.pair (Random.int 0 3) (Random.float 10 10.3))
--> [(0, 10.23412), (2, 10.17094)]
Das Paket elm-community/random-extra bietet viel mehr Generatoren für verschiedene Datenstrukturen: Strings, Datumsangaben, Wörterbücher, Arrays, Mengen usw.
Mit Random.constant können wir Generatoren erstellen, die immer nur einen einzigen Wert zurückgeben:
generate 4 (Random.constant "hello")
--> ["hello", "hello", "hello", "hello"]
Mit Random.uniform können wir mit gleicher Wahrscheinlichkeit zufällig aus den angegebenen Elementen auswählen:
generate 5 (Random.uniform Red [Green, Blue])
--> [Red, Blue, Blue, Green, Red]
Random.uniform nimmt zwei Argumente (Red und [Green, Blue]), um sicherzustellen, dass es mindestens einen Wert zum Auswählen gibt, denn eine einzelne Liste könnte leer sein.
Mit Random.weighted können wir auch die Wahrscheinlichkeiten anpassen:
generate 5 (Random.weighted (80, Red) [(15, Green), (5, Blue)])
--> [Red, Red, Green, Red, Red]
Die Werte müssen sich nicht zu 100 summieren, sie werden sowieso neu normalisiert.
Mit Random.map können wir ins Innere eines Generators gelangen:
generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10))
--> [30, 60, 10]
Wir können auch Random.map2 bis hin zu Random.map5 verwenden, um mehr Generatoren zu kombinieren:
position =
Random.map3
(\x y z -> Position x y z)
(Random.float -100 100)
(Random.float -100 100)
(Random.float -100 100)
generate 1 position
--> [Position 33.788 -98.321 10.0845]
Für komplexere Fälle gibt es Random.andThen : (a -> Generator b) -> Generator a -> Generator b, das den von einem Generator erzeugten Wert nutzen kann, um einen anderen zu erstellen:
bool = Random.uniform True [False]
failHalfOfTheTime : Generator a -> Generator (Maybe a)
failHalfOfTheTime generator =
bool
|> Random.andThen
(\boolResult ->
if boolResult then
Random.map Just generator
else
Random.constant Nothing
)
generate 6 (Random.int 0 1 |> failHalfOfTheTime)
--> [Nothing, Just 1, Just 0, Nothing, Just 1, Nothing]
Manchmal ist es nützlich, einen Generator selbstrekursiv zu definieren.
In solchen Fällen musst du möglicherweise Random.lazy verwenden, damit der Compiler den Generator nicht unendlich weit entfaltet.
type Peano
= Zero
| Next Peano
peano : Generator Peano
peano =
Random.uniform (Random.constant Zero)
[ Random.map Next (Random.lazy (\_ -> peano))
]
|> Random.andThen identity
generate 12 peano
--> [Zero, Next(Next(Zero)), Zero, Next(Zero), Zero, Zero, Next(Zero), Zero, Zero, Next(Zero), Next(Next(Next(Zero)))]
Dieses Beispiel ist etwas schwer, zerlegen wir es also.
Peano ist ein Typ, der positive Ganzzahlen darstellt: Zero ist 0, Next(Zero) ist 1, Next(Next(Zero)) ist 2 usw.
Wir definieren peano, um eine zufällige Peano-Zahl zu bekommen.
Zunächst einmal: Random.lazy fügt der Logik nichts hinzu, es verhindert nur, dass der Compiler peano endlos in peano hineinschreibt.
Wir verwenden Random.uniform, um zwischen null (mit 50 % Wahrscheinlichkeit) und einer weiteren Peano-Zahl plus eins (mit 50 % Wahrscheinlichkeit) zu wählen.
Anders als im vorherigen Beispiel wählt Random.uniform jedoch nicht aus Werten (wie Zero), sondern aus Generatoren (wie Random.constant Zero), weil wir peano selbst verwenden wollen.
Das bedeutet, dass Random.uniform einen Wert vom Typ Generator (Generator Peano) zurückgibt, und den brauchen wir nicht.
Um den Generator zu „flachen“, leiten wir ihn in Random.andThen : (a -> Generator b) -> Generator a -> Generator b, wobei b gleich Peano sein soll.
Da Generator a gleich Generator (Generator Peano) ist, muss a gleich Generator Peano sein, und die Funktion (a -> Generator b) muss vom Typ (Generator Peano -> Generator Peano) sein.
Wir wollen nichts verändern, also ist identity die richtige Wahl.
Und schließlich: Was für Zahlen wird peano erzeugen?
Wir wissen, dass es in 50 % der Fälle 0 erzeugt und in 50 % der Fälle eine weitere Iteration von sich selbst plus eins.
Das heißt, die Zahlen sind 0 (in 50 % der Fälle), 1 (in 25 % der Fälle), 2 (in 12 % der Fälle), 3 (in 6 % der Fälle) usw.
peano kann beliebig große Zahlen erzeugen, aber mit exponentiell abnehmender Wahrscheinlichkeit.
Du bist Labyrinth-Forscher und arbeitest an einer neuen Generation von Verliesen und Abenteuergebieten.
Du bist Theoretiker, das heißt, du kümmerst dich nicht um den physischen Aufbau von Labyrinthen (das ist eine Aufgabe für Labyrinth-Ingenieure), sondern um ihre mathematische Komplexität: Verzweigungsfaktor, maximale Tiefe und Ähnliches.
Du beschließt, dein nächstes Projekt mit einem Ansatz der zufälligen Generierung anzugehen.
Ein gutes Labyrinth besteht größtenteils aus Sackgassen.
Definiere den deadend-Generator so, dass er immer die Variante DeadEnd erzeugt.
Geben wir den Abenteurern einen Grund, sich in deine Labyrinthe zu wagen.
Definiere den treasure-Generator so, dass er mit gleicher Wahrscheinlichkeit entweder Gold, Diamond oder Friendship erzeugt.
Definiere dann den room-Generator so, dass er die von treasure erzeugten Werte in die Variante Room einpackt.
Und jetzt zum Herzstück eines Labyrinths: den Verzweigungen.
Der branch-Generator erhält ein Argument Generator Maze, also einen Generator, der ein beliebiges Labyrinth erzeugen kann.
Definiere den branch-Generator so, dass er eine Variante Branch erzeugt, die mehrere Verzweigungen enthält, die zu Labyrinthen führen, welche von seinem Argument erzeugt werden.
Die Anzahl der Verzweigungen sollte zwischen 2 und 4 liegen (bei mehr würde der Labyrinth-Ingenieur wütend werden) und mit gleicher Wahrscheinlichkeit zufällig bestimmt werden.
Jetzt wird es ernst.
Definiere den maze-Generator so, dass er in 60 % der Fälle eine Sackgasse erzeugt, in 15 % der Fälle einen Schatzraum und in 25 % der Fälle eine Verzweigung.
Die Verzweigungen sollten mit maze rekursiv neue Labyrinthe erzeugen.
Achte darauf, deadend, room und branch im Generator zu verwenden.
Jede Verzweigung enthält durchschnittlich 3 Labyrinthe, aber nur 25 % davon verzweigen sich erneut, was weniger als eins ist. Deine erzeugten Labyrinthe enden also wahrscheinlich irgendwann.
Dein maze ist nett, hat aber ein paar Probleme:
MazeMakerSupport.mazeDepth berechnet)Definiere den Generator mazeOfDepth n so, dass er ein Labyrinth der Tiefe n erzeugt.
Nur die tiefste Ebene 0 sollte Sackgassen oder Schatzräume enthalten (mit gleicher Wahrscheinlichkeit), alle anderen Ebenen sollten eine Verzweigung sein, die Labyrinthe der Tiefe n-1 enthält, welche rekursiv mit mazeOfDepth erzeugt werden.
Achte darauf, deadend, room und branch im Generator zu verwenden.
Melde dich bei Exercism an, um Elm mit 28 Konzepte110 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.