In un linguaggio puramente funzionale come Elm, una funzione chiamata con gli stessi argomenti deve restituire sempre lo stesso valore.
Di conseguenza, una funzione con la firma di tipo rand : Int può essere implementata solo come rand = 4, il che non promette nulla di buono se dobbiamo generare numeri interi casuali.
Come facciamo allora a generare valori casuali in Elm?
Dividiamo il problema in due: prima descriviamo il valore che vogliamo generare con un Random.Generator a, poi generiamo un valore.
Il primo modo per generare un valore è creare un Random.Seed tramite initialSeed : Int -> Seed e poi usare step : Generator a -> Seed -> ( a, Seed ), che restituisce un valore e un nuovo seed.
Nota che entrambe queste funzioni sono pure, quindi chiamarle due volte con gli stessi argomenti produrrà gli stessi valori.
Il secondo modo per generare un valore è usare generate : (a -> msg) -> Generator a -> Cmd msg, ma questo si può fare solo all'interno di un'applicazione Elm.
In quel caso, il runtime di Elm può usare step oltre a risorse esterne, non pure, per generare i seed.
D'ora in poi ci concentreremo sui generatori.
Solo per mostrare qualche esempio, facciamo finta di aver definito con initialSeed e step una funzione generate : Int -> Generator a -> List a che genera un certo numero di valori a partire da un generatore.
Il modulo Random fornisce due generatori di base per generare numeri interi e numeri in virgola mobile all'interno di un intervallo specifico:
generate 5 (Random.int -5 5)
--> [0, 3, -5, 5, 0]
generate 3 (Random.float 0 5)
--> [1.61803, 3.14159, 2.71828]
Quei valori si possono combinare in tuple, oppure in liste di valori:
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)]
Il pacchetto elm-community/random-extra fornisce molti più generatori per varie strutture dati: stringhe, date, dizionari, array, set e così via.
Possiamo creare generatori che restituiscono sempre un unico valore usando Random.constant:
generate 4 (Random.constant "hello")
--> ["hello", "hello", "hello", "hello"]
Possiamo scegliere a caso tra gli elementi dati con la stessa probabilità usando Random.uniform:
generate 5 (Random.uniform Red [Green, Blue])
--> [Red, Blue, Blue, Green, Red]
Random.uniform prende due argomenti (Red e [Green, Blue]) per garantire che ci sia almeno un valore tra cui scegliere, dato che una singola lista potrebbe essere vuota.
Possiamo anche ritoccare le probabilità usando Random.weighted:
generate 5 (Random.weighted (80, Red) [(15, Green), (5, Blue)])
--> [Red, Red, Green, Red, Red]
I valori non devono per forza sommare a 100, verranno comunque rinormalizzati.
Possiamo raggiungere l'interno di un generatore con Random.map:
generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10))
--> [30, 60, 10]
Possiamo anche usare da Random.map2 fino a Random.map5 per combinare più generatori:
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]
Per usi più complessi abbiamo Random.andThen : (a -> Generator b) -> Generator a -> Generator b, che può usare il valore generato da un generatore per crearne un altro:
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]
A volte è utile definire un generatore in modo ricorsivo su se stesso.
In quei casi, potresti aver bisogno di usare Random.lazy per impedire al compilatore di srotolare il generatore all'infinito.
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)))]
Questo esempio è un po' pesante, quindi analizziamolo pezzo per pezzo.
Peano è un tipo che rappresenta i numeri interi positivi: Zero è 0, Next(Zero) è 1, Next(Next(Zero)) è 2, e così via.
Definiamo peano in modo che ci dia un numero Peano casuale.
Prima di tutto, nota che Random.lazy non aggiunge nulla alla logica: impedisce soltanto al compilatore di scrivere peano dentro peano all'infinito.
Usiamo Random.uniform per scegliere tra zero (con probabilità del 50%) e un altro numero Peano più uno (con probabilità del 50%).
A differenza dell'esempio precedente, però, Random.uniform non sceglie tra valori (come Zero) ma tra generatori (come Random.constant Zero), dato che vogliamo usare peano stesso.
Questo significa che Random.uniform restituirà un valore di tipo Generator (Generator Peano), che non è quello che ci serve.
Per «appiattire» il generatore, lo passiamo a Random.andThen : (a -> Generator b) -> Generator a -> Generator b, dove vogliamo che b sia Peano.
Dato che Generator a è Generator (Generator Peano), a deve essere Generator Peano, e la funzione (a -> Generator b) deve essere di tipo (Generator Peano -> Generator Peano).
Non vogliamo modificare nulla, quindi identity è la scelta giusta.
Infine, che tipo di numeri produrrà peano?
Sappiamo che produrrà 0 nel 50% dei casi, oppure un'altra iterazione di se stesso più uno nell'altro 50% dei casi.
Questo significa che i numeri saranno 0 (nel 50% dei casi), 1 (nel 25% dei casi), 2 (nel 12% dei casi), 3 (nel 6% dei casi), e così via.
peano può produrre numeri arbitrariamente grandi, ma con probabilità che diminuisce in modo esponenziale.
Sei un ricercatore di labirinti e lavori per progettare una nuova generazione di sotterranei e terreni d'avventura.
Sei un teorico, il che significa che non ti occupi della disposizione fisica dei labirinti (quello è un compito per gli ingegneri dei labirinti), ma ti interessi della loro complessità matematica: fattore di ramificazione, profondità massima e simili.
Decidi di affrontare il prossimo progetto con un approccio di generazione casuale.
Un buon labirinto è fatto soprattutto di vicoli ciechi.
Definisci il generatore deadend in modo che generi sempre la variante DeadEnd.
Diamo agli avventurieri una ragione per avventurarsi nei labirinti.
Definisci il generatore treasure in modo che generi Gold, Diamond o Friendship con uguale probabilità.
Poi, definisci il generatore room in modo che racchiuda i valori generati da treasure nella variante Room.
E ora il cuore e l'anima di un labirinto: le diramazioni.
Il generatore branch riceve un argomento Generator Maze, che è un generatore in grado di generare un labirinto arbitrario.
Definisci il generatore branch in modo che generi una variante Branch contenente diverse diramazioni che portano a labirinti generati dal suo argomento.
Il numero di diramazioni dovrebbe essere compreso tra 2 e 4 (di più e l'ingegnere dei labirinti si arrabbierebbe), determinato casualmente con uguale probabilità.
È ora di fare sul serio.
Definisci il generatore maze in modo che generi un vicolo cieco nel 60% dei casi, una stanza del tesoro nel 15% dei casi e una diramazione nel 25% dei casi.
Le diramazioni dovrebbero generare nuovi labirinti usando maze ricorsivamente.
Assicurati di usare deadend, room e branch nel generatore.
Ogni diramazione contiene in media 3 labirinti, ma solo il 25% di questi si diramerà di nuovo, che è meno di uno, quindi è probabile che i labirinti generati finiscano prima o poi.
Il generatore maze è carino, ma ha alcuni problemi:
MazeMakerSupport.mazeDepth)Definisci il generatore mazeOfDepth n in modo che generi un labirinto di profondità n.
Solo il livello più profondo 0 dovrebbe contenere vicoli ciechi o stanze del tesoro (con uguale probabilità); tutti gli altri livelli dovrebbero essere una diramazione contenente labirinti di profondità n-1 generati ricorsivamente con mazeOfDepth.
Assicurati di usare deadend, room e branch nel generatore.
Iscriviti a Exercism per imparare e padroneggiare Elm con 28 concetti110 esercizi e il mentoring di persone reali, tutto gratis.