No puede crear una función pura llamada random
que dará un resultado diferente cada vez que se llama. De hecho, ni siquiera puede "llamar" funciones puras. Los aplicas. Así que no te estás perdiendo nada, pero esto no significa que los números aleatorios estén fuera de los límites en la programación funcional. Permítame demostrar, usaré la sintaxis de Haskell en todo momento.
Viniendo de un contexto imperativo, inicialmente puede esperar que aleatorio tenga un tipo como este:
random :: () -> Integer
Pero esto ya se ha descartado porque el azar no puede ser una función pura.
Considere la idea de un valor. Un valor es una cosa inmutable. Nunca cambia y cada observación que puede hacer al respecto es coherente en todo momento.
Claramente, aleatorio no puede producir un valor entero. En cambio, produce una variable aleatoria entera. Su tipo podría verse así:
random :: () -> Random Integer
Excepto que pasar un argumento es completamente innecesario, las funciones son puras, por lo que una random ()
es tan buena como la otra random ()
. Daré al azar, de aquí en adelante, este tipo:
random :: Random Integer
Lo cual está muy bien, pero no es muy útil. Es posible que pueda escribir expresiones como random + 42
, pero no puede, porque no va a escribir. No puedes hacer nada con variables aleatorias, todavía.
Ésto plantea una pregunta interesante. ¿Qué funciones deberían existir para manipular variables aleatorias?
Esta función no puede existir:
bad :: Random a -> a
de alguna manera útil, porque entonces podrías escribir:
badRandom :: Integer
badRandom = bad random
Lo que introduce una inconsistencia. Se supone que badRandom es un valor, pero también es un número aleatorio; Una contradicción.
Quizás deberíamos agregar esta función:
randomAdd :: Integer -> Random Integer -> Random Integer
Pero esto es solo un caso especial de un patrón más general. Debería poder aplicar cualquier función a una cosa aleatoria para obtener otras cosas aleatorias como esta:
randomMap :: (a -> b) -> Random a -> Random b
En lugar de escribir random + 42
, ahora podemos escribir randomMap (+42) random
.
Si todo lo que tuviera fuera randomMap, no podría combinar variables aleatorias. No podría escribir esta función, por ejemplo:
randomCombine :: Random a -> Random b -> Random (a, b)
Puedes intentar escribirlo así:
randomCombine a b = randomMap (\a' -> randomMap (\b' -> (a', b')) b) a
Pero tiene el tipo incorrecto. En lugar de terminar con un Random (a, b)
, terminamos con unRandom (Random (a, b))
Esto se puede solucionar agregando otra función:
randomJoin :: Random (Random a) -> Random a
Pero, por razones que eventualmente pueden quedar claras, no voy a hacer eso. En cambio, voy a agregar esto:
randomBind :: Random a -> (a -> Random b) -> Random b
No es inmediatamente obvio que esto realmente resuelva el problema, pero lo hace:
randomCombine a b = randomBind a (\a' -> randomMap (\b' -> (a', b')) b)
De hecho, es posible escribir randomBind en términos de randomJoin y randomMap. También es posible escribir randomJoin en términos de randomBind. Pero, me iré haciendo esto como ejercicio.
Podríamos simplificar esto un poco. Permítame definir esta función:
randomUnit :: a -> Random a
randomUnit convierte un valor en una variable aleatoria. Esto significa que podemos tener variables aleatorias que en realidad no son aleatorias. Sin embargo, este siempre fue el caso; podríamos haberlo hecho randomMap (const 4) random
antes. La razón por la cual randomUnit es una buena idea es que ahora podemos definir randomMap en términos de randomUnit y randomBind:
randomMap :: (a -> b) -> Random a -> Random b
randomMap f x = randomBind x (randomUnit . f)
Ok, ahora estamos llegando a alguna parte. Tenemos variables aleatorias que podemos manipular. Sin embargo:
- No es obvio cómo podríamos implementar estas funciones,
- Es bastante engorroso.
Implementación
Abordaré números pseudoaleatorios. Es posible implementar estas funciones para números aleatorios reales, pero esta respuesta ya se está haciendo bastante larga.
Esencialmente, la forma en que esto funcionará es que vamos a pasar un valor semilla por todas partes. Cada vez que generamos un nuevo valor aleatorio, produciremos una nueva semilla. Al final, cuando hayamos terminado de construir una variable aleatoria, querremos muestrearla usando esta función:
runRandom :: Seed -> Random a -> a
Voy a definir el tipo aleatorio de esta manera:
data Random a = Random (Seed -> (Seed, a))
Entonces, solo necesitamos proporcionar implementaciones de randomUnit, randomBind, runRandom y random, que es bastante sencillo:
randomUnit :: a -> Random a
randomUnit x = Random (\seed -> (seed, x))
randomBind :: Random a -> (a -> Random b) -> Random b
randomBind (Random f) g =
Random (\seed ->
let (seed', x) = f seed
Random g' = g x in
g' seed')
runRandom :: Seed -> Random a -> a
runRandom seed (Random f) = (snd . f) seed
Para el azar, voy a suponer que ya hay una función del tipo:
psuedoRandom :: Seed -> (Seed, Integer)
En cuyo caso el azar es justo Random psuedoRandom
.
Hacer las cosas menos engorrosas
Haskell tiene azúcar sintáctica para hacer que estas cosas sean más agradables para los ojos. Se llama do-notation y para usarlo todo lo que tenemos que hacer es crear una instancia de Monad for Random.
instance Monad Random where
return = randomUnit
(>>=) = randomBind
Hecho. randomCombine
de antes ahora podría escribirse:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = do
a' <- a
b' <- b
return (a', b')
Si estuviera haciendo esto por mí mismo, incluso iría un paso más allá y crearía una instancia de aplicativo. (No se preocupe si esto no tiene sentido).
instance Functor Random where
fmap = liftM
instance Applicative Random where
pure = return
(<*>) = ap
Entonces randomCombine podría escribirse:
randomCombine :: Random a -> Random b -> Random (a, b)
randomCombine a b = (,) <$> a <*> b
Ahora que tenemos estas instancias, podemos usar en >>=
lugar de randomBind, join en lugar de randomJoin, fmap en lugar de randomMap, return en lugar de randomUnit. También obtenemos una gran cantidad de funciones de forma gratuita.
¿Vale la pena? Se podría argumentar que llegar a esta etapa, donde trabajar con números aleatorios no es completamente horrendo, fue bastante difícil y largo. ¿Qué obtuvimos a cambio de este esfuerzo?
La recompensa más inmediata es que ahora podemos ver exactamente qué partes de nuestro programa dependen de la aleatoriedad y qué partes son completamente deterministas. En mi experiencia, forzar una separación estricta como esta simplifica enormemente las cosas.
Asumimos hasta ahora que solo queremos una única muestra de cada variable aleatoria que generamos, pero si resulta que en el futuro realmente nos gustaría ver más de la distribución, esto es trivial. Puede usar runRandom muchas veces en la misma variable aleatoria con diferentes semillas. Esto es posible, por supuesto, en lenguajes imperativos, pero en este caso, podemos estar seguros de que no realizaremos IO no anticipadas cada vez que muestreemos una variable aleatoria y no tengamos que tener cuidado con la inicialización del estado.
bad :: Random a -> a
introducir inconsistencias? ¿Qué tiene de malo? Vaya lentamente en la explicación, especialmente para los primeros pasos :) Si pudiera explicar por qué las funciones "útiles" son útiles, ¡esta podría ser una respuesta de 1000 puntos! :)Tu no estas equivocado. Si le da la misma semilla a un RNG dos veces, entonces el primer número pseudoaleatorio que devuelve será el mismo. Esto no tiene nada que ver con la programación funcional versus la de efectos secundarios; La definición de una semilla es que una entrada específica causa una salida específica de valores bien distribuidos pero decididamente no aleatorios. Es por eso que se llama pseudoaleatorio, y a menudo es bueno tenerlo, por ejemplo, para escribir pruebas unitarias predecibles, para comparar de manera confiable diferentes métodos de optimización en el mismo problema, etc.
Si realmente desea números no pseudoaleatorios de una computadora, debe conectarlo a algo realmente aleatorio, como una fuente de descomposición de partículas, eventos impredecibles que ocurren dentro de la red en la que está la computadora, etc. Esto es difícil de es correcto y generalmente costoso incluso si funciona, pero es la única forma de no obtener valores pseudoaleatorios (generalmente los valores que recibe de su lenguaje de programación se basan en alguna semilla, incluso si no proporcionó uno explícitamente).
Esto, y solo esto, comprometería la naturaleza funcional de un sistema. Dado que los generadores no pseudoaleatorios son raros, esto no ocurre a menudo, pero sí, si realmente tiene un método que genera números aleatorios verdaderos, entonces al menos esa pequeña parte de su lenguaje de programación no puede ser 100% puramente funcional. Si un lenguaje sería una excepción para él o no es solo una cuestión de cuán pragmático es el implementador del lenguaje.
fuente
() -> Integer
. Puede tener un PRNG de tipo puramente funcionalPRNG_State -> (PRNG_State, Integer)
, pero deberá inicializarlo por medios impuros).Una forma es pensarlo como una secuencia infinita de números aleatorios:
Es decir, solo piense en ella como una estructura de datos sin fondo, como un lugar
Stack
donde solo puede llamarPop
, pero puede llamarlo para siempre. Al igual que una pila inmutable normal, sacar una de la parte superior te da otra pila (diferente).Por lo tanto, un generador de números aleatorios inmutable (con evaluación diferida) podría verse así:
Eso es funcional
fuente
pseudoRandom :: Seed -> (Seed, Integer)
. Incluso podría terminar escribiendo una función de este tipo[Integer] -> ([Integer], Integer)
Es lo mismo para los lenguajes no funcionales. Ignorando el problema ligeramente separado de los números verdaderamente aleatorios aquí.
Un generador de números aleatorios siempre toma un valor de inicialización y para la misma inicialización devuelve la misma secuencia de números aleatorios (muy útil si necesita probar un programa que utiliza números aleatorios). Básicamente comienza con la semilla que eliges y luego usa el último resultado como semilla para la próxima iteración. Así que la mayoría de las implementaciones son funciones "puras" como las describe: tome un valor y para el mismo valor siempre devuelve el mismo resultado.
fuente