En un lenguaje funcional puro como Elm, una función a la que se llama con los mismos argumentos siempre debe devolver el mismo valor.
Por lo tanto, una función con la firma de tipo rand : Int solo puede implementarse como rand = 4, lo cual no augura nada bueno para generar números enteros aleatorios.
Entonces, ¿cómo generamos valores aleatorios en Elm?
Dividimos el problema en dos: primero describimos el valor que queremos generar con un Random.Generator a, y luego generamos un valor.
La primera forma de generar un valor es crear una Random.Seed con initialSeed : Int -> Seed y luego usar step : Generator a -> Seed -> ( a, Seed ), que devuelve un valor y una nueva semilla.
Ten en cuenta que ambas funciones son puras, así que llamarlas dos veces con los mismos argumentos producirá los mismos valores.
La segunda forma de generar un valor es usar generate : (a -> msg) -> Generator a -> Cmd msg, pero eso solo se puede hacer dentro de una aplicación de Elm.
En ese caso, el entorno de ejecución de Elm puede usar step, además de recursos externos no puros, para generar semillas.
De ahora en adelante, nos centraremos en los generadores.
Imaginemos, para poder mostrar ejemplos, que definimos con initialSeed y step una función generate : Int -> Generator a -> List a que genera cierta cantidad de valores a partir de un generador.
El módulo Random proporciona dos generadores básicos para generar números enteros y números de punto flotante dentro de un rango específico:
generate 5 (Random.int -5 5)
--> [0, 3, -5, 5, 0]
generate 3 (Random.float 0 5)
--> [1.61803, 3.14159, 2.71828]
Esos valores se pueden combinar en tuplas o en listas de valores:
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)]
El paquete elm-community/random-extra proporciona muchos más generadores para diversas estructuras de datos: strings, fechas, diccionarios, arrays, conjuntos, etc.
Podemos crear generadores que solo devuelvan un único valor usando Random.constant:
generate 4 (Random.constant "hello")
--> ["hello", "hello", "hello", "hello"]
Podemos elegir aleatoriamente entre elementos dados con la misma probabilidad usando Random.uniform:
generate 5 (Random.uniform Red [Green, Blue])
--> [Red, Blue, Blue, Green, Red]
Random.uniform recibe dos argumentos (Red y [Green, Blue]) para garantizar que haya al menos un valor del que elegir, ya que una sola lista podría estar vacía.
También podemos ajustar las probabilidades usando Random.weighted:
generate 5 (Random.weighted (80, Red) [(15, Green), (5, Blue)])
--> [Red, Red, Green, Red, Red]
Los valores no necesitan sumar 100, de todos modos se van a renormalizar.
Podemos acceder al interior de un generador con Random.map:
generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10))
--> [30, 60, 10]
También podemos usar Random.map2 hasta Random.map5 para combinar más generadores:
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]
Para usos más complejos, tenemos Random.andThen : (a -> Generator b) -> Generator a -> Generator b, que puede usar el valor generado por un generador para crear otro:
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 veces es útil definir un generador de forma autorrecursiva.
En esos casos, puede que necesites usar Random.lazy para evitar que el compilador expanda el generador de forma infinita.
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)))]
Este ejemplo es un poco denso, así que analicémoslo por partes.
Peano es un tipo que representa números enteros positivos: Zero es 0, Next(Zero) es 1, Next(Next(Zero)) es 2, etc.
Definimos peano para que nos dé un número Peano aleatorio.
En primer lugar, ten en cuenta que Random.lazy no aporta nada a la lógica; simplemente evita que el compilador escriba peano dentro de peano de forma indefinida.
Usamos Random.uniform para elegir entre cero (con un 50% de probabilidad) y otro número Peano más uno (con un 50% de probabilidad).
Sin embargo, a diferencia del ejemplo anterior, Random.uniform no elige entre valores (como Zero), sino entre generadores (como Random.constant Zero), ya que queremos usar peano mismo.
Esto significa que Random.uniform devolverá un valor de tipo Generator (Generator Peano), que no es lo que necesitamos.
Para «aplanar» el generador, lo canalizamos hacia Random.andThen : (a -> Generator b) -> Generator a -> Generator b, donde queremos que b sea Peano.
Como Generator a es Generator (Generator Peano), a debe ser Generator Peano, y la función (a -> Generator b) debe ser de tipo (Generator Peano -> Generator Peano).
No queremos modificar nada, así que identity es la opción correcta.
Por último, ¿qué tipo de números producirá peano?
Sabemos que producirá 0 el 50% de las veces, o bien otra iteración de sí mismo más uno el otro 50% de las veces.
Eso significa que los números serán 0 (el 50% de las veces), 1 (el 25% de las veces), 2 (el 12% de las veces), 3 (el 6% de las veces), etc.
peano puede producir números arbitrariamente grandes, pero con una probabilidad que disminuye de forma exponencial.
Eres un investigador de laberintos y trabajas en diseñar una nueva generación de mazmorras y terrenos de aventura.
Eres un teórico, lo que significa que no te interesa la disposición física de los laberintos (eso es tarea de los ingenieros de laberintos), sino su complejidad matemática: el factor de ramificación, la profundidad máxima y cosas por el estilo.
Decides abordar tu próximo proyecto con un enfoque de generación aleatoria.
Cualquier laberinto que se precie es, en su mayoría, callejones sin salida.
Define el generador deadend para que siempre genere la variante DeadEnd.
Vamos a darles a los aventureros una razón para adentrarse en tus laberintos.
Define el generador treasure para que genere Gold, Diamond o Friendship con la misma probabilidad.
Luego, define el generador room para que envuelva los valores generados por treasure en la variante Room.
Y ahora, el corazón y el alma de un laberinto: las ramas.
El generador branch recibe un argumento Generator Maze, que es un generador capaz de generar un laberinto arbitrario.
Define el generador branch para que genere una variante Branch que contenga varias ramas que lleven a laberintos generados por su argumento.
La cantidad de ramas debe estar entre 2 y 4 (si hay más, el ingeniero de laberintos se enojaría), determinada al azar con la misma probabilidad.
Es hora de ponerse manos a la obra.
Define el generador maze para que genere un callejón sin salida el 60% de las veces, una sala del tesoro el 15% de las veces y una rama el 25% de las veces.
Las ramas deben generar nuevos laberintos usando maze de forma recursiva.
Asegúrate de usar deadend, room y branch en el generador.
Cada rama contiene, en promedio, 3 laberintos, pero solo el 25% de estos se ramificará de nuevo, lo que da menos de uno, así que es probable que tus laberintos generados terminen en algún momento.
Tu maze está bien, pero tiene algunos problemas:
MazeMakerSupport.mazeDepth)Define el generador mazeOfDepth n para que genere un laberinto de profundidad n.
Únicamente el nivel más profundo, 0, debe contener callejones sin salida o salas del tesoro (con la misma probabilidad); todos los demás niveles deben ser una rama que contenga laberintos de profundidad n-1 generados de forma recursiva con mazeOfDepth.
Asegúrate de usar deadend, room y branch en el generador.
Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.