haskell - que - monadologia leibniz pdf
¿Por qué mónadas? ¿Cómo resuelve los efectos secundarios? (7)
Estoy aprendiendo a Haskell y tratando de entender las Mónadas. Tengo 2 preguntas
Por lo que entiendo, Monad es simplemente otra clase de tipos que declara maneras de interactuar con datos dentro de "contenedores", incluidos Maybes, listas y IO. Parece ingenioso y limpio implementar estas 3 cosas con un concepto, pero realmente, el punto es que puede haber un manejo limpio de errores en una cadena de funciones, contenedores y efectos secundarios. ¿Es esta una interpretación correcta?
En segundo lugar, ¿cómo se resuelve exactamente el problema de los efectos secundarios? Con este concepto de contenedores, el lenguaje básicamente dice que cualquier cosa dentro de los contenedores no es determinista (como I / O). Debido a que las listas y los IO son contenedores, las listas están clasificadas por equivalencia con IO, a pesar de que los valores dentro de las listas me parecen bastante deterministas. Entonces, ¿qué es determinista y qué tiene efectos secundarios? No puedo entender la idea de que un valor básico es determinista, hasta que lo colocas en un contenedor (que no es más especial que el mismo valor con algunos otros valores al lado, por ejemplo, Nothing) y ahora puede ser aleatorio .
¿Alguien puede explicar cómo, intuitivamente, Haskell logra cambiar el estado con entradas y salidas? No estoy viendo la magia aquí.
Con este concepto de contenedores, el lenguaje básicamente dice que cualquier cosa dentro de los contenedores no es determinista
No. Haskell es determinista. Si pide la suma entera de 2 + 2, siempre obtendrá 4.
"No determinista" es solo una metáfora, una forma de pensar. Todo es determinista bajo el capó. Si tienes este código:
do x <- [4,5]
y <- [0,1]
return (x+y)
es más o menos equivalente al código de Python
l = []
for x in [4,5]:
for y in [0,1]:
l.append(x+y)
¿Ves el no determinismo aquí? No, es la construcción determinista de una lista. Ejecútelo dos veces, obtendrá los mismos números en el mismo orden.
Puede describirlo de esta manera: elija arbitrariamente x de [4,5]. Elija arbitraria y de [0,1]. Devuelve x + y. Recoge todos los resultados posibles.
De esa manera parece implicar el no determinismo, pero es solo un ciclo anidado (comprensión de la lista). Aquí no hay un determinismo "real", se simula al verificar todas las posibilidades. El no determinismo es una ilusión. El código solo parece ser no determinista.
Este código usa la mónada de estado:
do put 0
x <- get
put (x+2)
y <- get
return (y+3)
da 5 y parece implicar un cambio de estado. Al igual que con las listas, es una ilusión. No hay "variables" que cambien (como en los lenguajes imperativos). Todo es inmutable bajo el capó.
Puede describir el código de esta manera: ponga 0 en una variable. Lea el valor de una variable en x. Pon (x + 2) a la variable. Lee la variable a y, y devuelve y + 3.
De esta forma parece implicar estado, pero solo se trata de funciones de composición que pasan parámetros adicionales. No hay mutabilidad "real" aquí, se simula por composición. La mutabilidad es una ilusión. El código solo parece estar usándolo.
Haskell lo hace de esta manera: tienes funciones
a -> s -> (b,s)
Esta función toma el valor antiguo de estado y devuelve un nuevo valor. No implica mutabilidad o variables de cambio. Es una función en el sentido matemático.
Por ejemplo, la función "poner" toma un nuevo valor de estado, ignora el estado actual y devuelve un nuevo estado:
put x _ = ((), x)
Al igual que usted puede componer dos funciones normales
a -> b
b -> c
dentro
a -> c
Con el operador (.) puede componer transformadores "estatales"
a -> s -> (b,s)
b -> s -> (c,s)
en una sola función
a -> s -> (c,s)
Intenta escribir el operador de composición tú mismo. Esto es lo que realmente sucede, no hay "efectos secundarios" que solo pasan argumentos a las funciones.
El punto es que puede haber un manejo limpio de errores en una cadena de funciones, contenedores y efectos secundarios. ¿Es esta una interpretación correcta?
Realmente no. Mencionó muchos conceptos que las personas citan al tratar de explicar las mónadas, incluidos los efectos secundarios, el manejo de errores y el no determinismo, pero parece que tiene el sentido incorrecto de que todos estos conceptos se aplican a todas las mónadas. Pero hay un concepto que mencionas que hace: encadenar .
Hay dos sabores diferentes de esto, así que lo explicaré de dos maneras diferentes: uno sin efectos secundarios y otro con efectos secundarios.
Sin efectos secundarios:
Toma el siguiente ejemplo:
addM :: (Monad m, Num a) => m a -> m a -> m a
addM ma mb = do
a <- ma
b <- mb
return (a + b)
Esta función agrega dos números, con el giro de que están envueltos en alguna mónada. ¿Qué mónada? ¡No importa! En todos los casos, ese especial do
sintaxis de los azucares a lo siguiente:
addM ma mb =
ma >>= /a ->
mb >>= /b ->
return (a + b)
... o, con la precedencia del operador explicita:
ma >>= (/a -> mb >>= (/b -> return (a + b)))
Ahora puedes ver realmente que se trata de una cadena de pequeñas funciones, todas juntas, y su comportamiento dependerá de cómo se definen >>=
y return
para cada mónada. Si está familiarizado con el polimorfismo en los lenguajes orientados a objetos, esto es esencialmente lo mismo: una interfaz común con múltiples implementaciones. Es un poco más alucinante que la interfaz OOP promedio, ya que la interfaz representa una política de cálculo en lugar de, por ejemplo, un animal o una forma o algo así.
De acuerdo, veamos algunos ejemplos de cómo addM
comporta en diferentes mónadas. La mónada de Identity
es un buen lugar para comenzar, ya que su definición es trivial:
instance Monad Identity where
return a = Identity a -- create an Identity value
(Identity a) >>= f = f a -- apply f to a
Entonces, ¿qué sucede cuando decimos:
addM (Identity 1) (Identity 2)
Ampliando esto, paso a paso:
(Identity 1) >>= (/a -> (Identity 2) >>= (/b -> return (a + b)))
(/a -> (Identity 2) >>= (/b -> return (a + b)) 1
(Identity 2) >>= (/b -> return (1 + b))
(/b -> return (1 + b)) 2
return (1 + 2)
Identity 3
Estupendo. Ahora, como mencionaste el manejo limpio de errores, veamos la Mónada Maybe
. Su definición es solo un poco más complicada que la Identity
:
instance Monad Maybe where
return a = Just a -- same as Identity monad!
(Just a) >>= f = f a -- same as Identity monad again!
Nothing >>= _ = Nothing -- the only real difference from Identity
Entonces, pueden imaginarse que si decimos addM (Just 1) (Just 2)
obtendremos Just 3
. Pero para addM Nothing (Just 1)
lugar:
Nothing >>= (/a -> (Just 1) >>= (/b -> return (a + b)))
Nothing
O al revés, addM (Just 1) Nothing
:
(Just 1) >>= (/a -> Nothing >>= (/b -> return (a + b)))
(/a -> Nothing >>= (/b -> return (a + b)) 1
Nothing >>= (/b -> return (1 + b))
Nothing
Entonces, la definición de Maybe
monad de >>=
se modificó para dar cuenta de la falla. Cuando se aplica una función a un valor Maybe
usando >>=
, obtienes lo que esperas.
De acuerdo, entonces mencionaste no determinismo. Sí, la lista de mónadas puede considerarse modelado de no determinismo en cierto sentido ... Es un poco raro, pero piense que la lista representa valores alternativos posibles: [1, 2, 3]
no es una colección, es una número único no determinista que podría ser uno, dos o tres. Eso suena tonto, pero comienza a tener sentido cuando piensas en cómo >>=
está definido para las listas: aplica la función dada a cada valor posible. Entonces addM [1, 2] [3, 4]
va a calcular todas las sumas posibles de esos dos valores no deterministas: [4, 5, 5, 6]
.
De acuerdo, ahora para abordar su segunda pregunta ...
Efectos secundarios:
Supongamos que aplica addM
a dos valores en la mónada IO
, como:
addM (return 1 :: IO Int) (return 2 :: IO Int)
No obtienes nada especial, solo 3 en la mónada IO
. addM
no lee ni escribe ningún estado mutable, por lo que no es divertido. Lo mismo ocurre con las mónadas State
o ST
. No es divertido. Entonces usemos una función diferente:
fireTheMissiles :: IO Int -- returns the number of casualties
Claramente, el mundo será diferente cada vez que se disparen misiles. Claramente. Ahora digamos que estás tratando de escribir un código totalmente inocuo, libre de efectos secundarios y que no genere proyectiles. Quizás intentes agregar dos números, pero esta vez sin mónadas volando:
add :: Num a => a -> a -> a
add a b = a + b
y, de repente, tu mano se resbala y accidentalmente escribes un error tipográfico:
add a b = a + b + fireTheMissiles
Un error honesto, realmente. Las llaves estaban muy juntas. Afortunadamente, debido a que fireTheMissiles
fue de tipo IO Int
lugar de simplemente Int
, el compilador puede evitar un desastre.
Bien, ejemplo totalmente inventado, pero el punto es que en el caso de IO
, ST
y amigos, el sistema de tipo mantiene los efectos aislados de un contexto específico. No elimina mágicamente los efectos secundarios, lo que hace que el código sea referencialmente transparente, pero no debe serlo, pero deja claro en el momento de la compilación a qué alcance se limitan los efectos.
Volviendo al punto original: ¿qué tiene que ver esto con el encadenamiento o la composición de funciones? Bueno, en este caso, es solo una manera útil de expresar una secuencia de efectos:
fireTheMissilesTwice :: IO ()
fireTheMissilesTwice = do
a <- fireTheMissiles
print a
b <- fireTheMissiles
print b
Resumen:
Una mónada representa una política para encadenar cálculos. Identity
política de Identity
es pura composición de funciones, la política de Maybe
es composición de funciones con propogación de fallas, la política de IO
es composición de funciones impura , etc.
el punto es que puede haber un manejo limpio de errores en una cadena de funciones, contenedores y efectos secundarios
Más o menos.
¿Cómo se resuelve el problema de los efectos colaterales?
Un valor en la mónada de E / S, es decir, uno de tipo IO a
, debe interpretarse como un programa. p >> q
en los valores IO
puede interpretarse entonces como el operador que combina dos programas en uno que primero ejecuta p
, luego q
. Los otros operadores de mónadas tienen interpretaciones similares. Al asignar un programa al nombre main
, declaras al compilador que ese es el programa que debe ejecutarse por su código de objeto de salida.
En cuanto a la lista de mónadas, no está realmente relacionada con la mónada de E / S, excepto en un sentido matemático muy abstracto. La mónada IO
proporciona un cómputo determinístico con efectos secundarios, mientras que la lista de mónadas proporciona búsquedas de rastreo no deterministas (¡pero no al azar!), Algo similar al modus operandi de Prolog.
Hay tres observaciones principales con respecto a la mónada IO:
1) No puedes obtener valores de eso. Otros tipos como Maybe
podrían permitir extraer valores, pero ni la interfaz de la clase de mónada en sí ni el tipo de datos IO
permiten.
2) "Inside" IO
no es solo el valor real, sino también la cosa de "RealWorld". Este valor ficticio se usa para imponer el encadenamiento de acciones por el sistema de tipo : si tiene dos cálculos independientes, el uso de >>=
hace que el segundo cálculo dependa del primero.
3) Suponga una cosa no determinista como random :: () -> Int
, que no está permitido en Haskell. Si cambia la firma a random :: Blubb -> (Blubb, Int)
, está permitido, si se asegura de que nadie pueda usar un Blubb
dos veces: porque en ese caso todas las entradas son "diferentes", no hay problema que las salidas son diferentes también
Ahora podemos usar el hecho 1): nadie puede obtener algo de IO
, por lo que podemos usar el dummy de RealWord
oculto en IO
para que funcione como un Blubb
. Solo hay una IO
en toda la aplicación (la que obtenemos de la main
), y se encarga de la secuenciación adecuada, como hemos visto en 2). Problema resuelto.
Permítanme comenzar señalando el excelente artículo " Podrías haber inventado las mónadas ". Ilustra cómo la estructura de Monad se puede manifestar de forma natural mientras escribes programas. Pero el tutorial no menciona IO
, así que tendré una puñalada aquí para ampliar el enfoque.
Comencemos con lo que probablemente ya haya visto: la mónada de contenedor. Digamos que tenemos:
f, g :: Int -> [Int]
Una forma de ver esto es que nos da una cantidad de resultados posibles para cada entrada posible. ¿Qué pasa si queremos todos los resultados posibles para la composición de ambas funciones? ¿Dando todas las posibilidades que podamos obtener aplicando las funciones una después de la otra?
Bueno, hay una función para eso:
fg x = concatMap g $ f x
Si ponemos esto más general, obtenemos
fg x = f x >>= g
xs >>= f = concatMap f xs
return x = [x]
¿Por qué querríamos envolverlo así? Bueno, escribir nuestros programas principalmente usando >>=
y return
nos da algunas buenas propiedades, por ejemplo, podemos estar seguros de que es relativamente difícil "olvidar" las soluciones. Tendríamos explícitamente que reintroducirlo, por ejemplo, agregando otro skip
función. ¡Y también ahora tenemos una mónada y podemos usar todos los combinadores de la biblioteca de mónadas!
Ahora, déjanos saltar a tu ejemplo más complicado. Digamos que las dos funciones son "efecto secundario". Eso no es no determinista, simplemente significa que, en teoría, todo el mundo es a la vez su entrada (ya que puede influir en ellos), así como su salida (ya que la función puede influir en ella). Entonces obtenemos algo como:
f, g :: Int -> RealWorld# -> (Int, RealWorld#)
Si ahora queremos f
para obtener el mundo que g
dejó atrás, escribiríamos:
fg x rw = let (y, rw'') = f x rw
(r, rw'''') = g y rw''
in (r, rw'''')
O generalizado:
fg x = f x >>= g
x >>= f = /rw -> let (y, rw'') = x rw
(r, rw'''') = f y rw''
in (r, rw'''')
return x = /rw -> (x, rw)
Ahora, si el usuario solo puede usar >>=
, return
y algunos valores de IO
predefinidos, obtenemos una buena propiedad de nuevo: ¡el usuario nunca verá realmente el RealWorld#
que se pasa! Y eso es algo muy bueno, ya que no está realmente interesado en los detalles de dónde getLine
obtiene sus datos. Y de nuevo obtenemos todas las agradables funciones de alto nivel de las bibliotecas de mónadas.
Entonces las cosas importantes para llevar:
La mónada captura patrones comunes en su código, como "pasar siempre todos los elementos del contenedor A al contenedor B" o "pasar esta etiqueta del mundo real". A menudo, una vez que te das cuenta de que hay una mónada en tu programa, las cosas complicadas se convierten simplemente en aplicaciones del combinador de mónada correcto.
La mónada le permite ocultar completamente la implementación del usuario. Es un excelente mecanismo de encapsulación, ya sea para su propio estado interno o para cómo
IO
logra exprimir la no pureza en un programa puro de una manera relativamente segura.
Apéndice
En caso de que alguien todavía esté rascándose la cabeza sobre RealWorld#
tanto como lo hice cuando comencé: Obviamente hay más magia pasando después de que se haya eliminado toda la abstracción de la mónada. Entonces el compilador utilizará el hecho de que solo puede haber un solo "mundo real". Esas son buenas y malas noticias:
Se deduce que el compilador debe garantizar el orden de ejecución entre funciones (¡y eso es lo que buscamos!)
Pero también significa que no es necesario pasar el mundo real, ya que solo hay uno que podríamos querer decir: ¡el que está activo cuando se ejecuta la función!
En pocas palabras, una vez que se ha arreglado el orden de ejecución, RealWorld#
simplemente se optimiza. Por lo tanto, los programas que usan la mónada IO
realmente tienen una sobrecarga de tiempo de ejecución cero. También tenga en cuenta que usar RealWorld#
es obviamente solo una forma posible de poner IO
, pero resulta ser el que GHC usa internamente. Lo bueno de las mónadas es que, de nuevo, el usuario realmente no necesita saber.
Podría ver una mónada determinada como un conjunto / familia (o dominio, dominio, etc.) de acciones (piense en una declaración C). La mónada m
define el tipo de efectos (laterales) que sus acciones pueden tener:
- con
[]
puede definir acciones que pueden bifurcar sus ejecuciones en diferentes "mundos paralelos independientes"; - con
Either Foo
puedes definir acciones que pueden fallar con errores de tipoFoo
; - con
IO
puede definir acciones que pueden tener efectos colaterales en el "mundo exterior" (acceder a archivos, red, iniciar procesos, hacer un HTTP GET ...); - puedes tener una mónada cuyo efecto sea "aleatoriedad" (ver el paquete
MonadRandom
); - puedes definir una mónada cuyas acciones pueden hacer un movimiento en un juego (por ejemplo, ajedrez, Go ...) y recibir un movimiento de un oponente, pero no pueden escribir en tu sistema de archivos ni en ninguna otra cosa.
Resumen
Si m
es una mónada, ma
es una acción que produce un resultado / salida de tipo a
.
Los operadores >>
y >>=
se usan para crear acciones más complejas de las más simples:
-
a >> b
es una acción macro que realiza la accióna
y luego la acciónb
; -
a >> a
hace accióna
y luego accióna
nuevamente; - con
>>=
la segunda acción puede depender de la salida de la primera.
El significado exacto de qué es una acción y qué hace una acción y luego otra depende de la mónada: cada mónada define una sublengua imperativa con algunas características / efectos.
Secuenciación simple ( >>
)
Digamos que tenemos una mónada M
dada y algunas acciones incrementCounter
, decrementCounter
, readCounter
:
instance M Monad where ...
-- Modify the counter and do not produce any result:
incrementCounter :: M ()
decrementCounter :: M ()
-- Get the current value of the counter
readCounter :: M Integer
Ahora nos gustaría hacer algo interesante con esas acciones. Lo primero que nos gustaría hacer con esas acciones es secuenciarlas. Como en decir C, nos gustaría poder hacer:
// This is C:
counter++;
counter++;
Definimos un "operador de secuenciación" >>
. Usando este operador podemos escribir:
incrementCounter >> incrementCounter
¿Cuál es el tipo de "incrementCounter >> incrementCounter"?
Es una acción hecha de dos acciones más pequeñas, como en C puedes escribir declaraciones compuestas a partir de enunciados atómicos:
// This is a macro statement made of several statements { counter++; counter++; } // and we can use it anywhere we may use a statement: if (condition) { counter++; counter++; }
puede tener el mismo tipo de efectos que sus subacciones;
no produce ninguna salida / resultado.
Por lo tanto, nos gustaría que incrementCounter >> incrementCounter
sea del tipo M ()
: una acción (macro) con el mismo tipo de efectos posibles pero sin ningún resultado.
De manera más general, dadas dos acciones:
action1 :: M a
action2 :: M b
definimos a a >> b
como la acción macro que se obtiene al hacer (lo que sea que eso signifique en nuestro dominio de acción) a
luego b
y produce como resultado el resultado de la ejecución de la segunda acción. El tipo de >>
es:
(>>) :: M a -> M b -> M b
o más generalmente:
(>>) :: (Monad m) => m a -> m b -> m b
Podemos definir una mayor secuencia de acciones a partir de las más simples:
action1 >> action2 >> action3 >> action4
Entradas y salidas ( >>=
)
Nos gustaría poder incrementar por otra cosa que sea 1 a la vez:
incrementBy 5
Queremos proporcionar algunos comentarios en nuestras acciones, para ello definimos un incrementBy
función Al tomar un Int
y producir una acción:
incrementBy :: Int -> M ()
Ahora podemos escribir cosas como:
incrementCounter >> readCounter >> incrementBy 5
Pero no tenemos forma de enviar la salida de readCounter
a incrementBy
. Para hacer esto, se necesita una versión un poco más poderosa de nuestro operador de secuenciación. El operador >>=
puede alimentar la salida de una acción dada como entrada a la siguiente acción. Podemos escribir:
readCounter >>= incrementBy
Es una acción que ejecuta la acción readCounter
, alimenta su resultado en la función incrementBy
y luego ejecuta la acción resultante.
El tipo de >>=
es:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Un ejemplo (parcial)
Digamos que tengo una mónada de Prompt
que solo puede mostrar información (texto) al usuario y solicitar información al usuario:
-- We don''t have access to the internal structure of the Prompt monad
module Prompt (Prompt(), echo, prompt) where
-- Opaque
data Prompt a = ...
instance Monad Prompt where ...
-- Display a line to the CLI:
echo :: String -> Prompt ()
-- Ask a question to the user:
prompt :: String -> Prompt String
Intentemos definir un promptBoolean message
acciones de promptBoolean message
que pide una pregunta y produce un valor booleano.
Usamos la acción prompt (message ++ "[y/n]")
y alimentamos su salida a una función f
:
f "y"
debería ser una acción que no hace más que producirTrue
como salida;f "n"
debería ser una acción que no hace más que producirFalse
como salida;cualquier otra cosa debería reiniciar la acción (hacer la acción de nuevo);
promptBoolean
se vería así:
-- Incomplete version, some bits are missing:
promptBoolean :: String -> M Boolean
promptBoolean message = prompt (message ++ "[y/n]") >>= f
where f result = if result == "y"
then ???? -- We need here an action which does nothing but produce `True` as output
else if result=="n"
then ???? -- We need here an action which does nothing but produce `False` as output
else echo "Input not recognised, try again." >> promptBoolean
Produciendo un valor sin efecto ( return
)
Para llenar los bits que faltan en nuestra función promptBoolean
, necesitamos una forma de representar acciones ficticias sin ningún efecto secundario pero que solo arroje un valor dado:
-- "return 5" is an action which does nothing but outputs 5
return :: (Monad m) => a -> m a
y ahora podemos escribir la función promptBoolean
:
promptBoolean :: String -> Prompt Boolean
promptBoolean message :: prompt (message ++ "[y/n]") >>= f
where f result = if result=="y"
then return True
else if result=="n"
then return False
else echo "Input not recognised, try again." >> promptBoolean message
Al componer esas dos acciones simples ( promptBoolean
, echo
) podemos definir cualquier tipo de diálogo entre el usuario y su programa (las acciones del programa son deterministas ya que nuestra mónada no tiene un "efecto de aleatoriedad").
promptInt :: String -> M Int
promptInt = ... -- similar
-- Classic "guess a number game/dialogue"
guess :: Int -> m()
guess n = promptInt "Guess:" m -> f
where f m = if m == n
then echo "Found"
else (if m > n
then echo "Too big"
then echo "Too small") >> guess n
Las operaciones de una mónada
Una mónada es un conjunto de acciones que se pueden componer con los operadores return
y >>=
:
>>=
para la composición de acción;return
para producir un valor sin ningún efecto (lateral).
Estos dos operadores son los operadores mínimos necesarios para definir una Monad
.
En Haskell, el operador >>
es necesario, pero de hecho puede derivarse de >>=
:
(>>): Monad m => m a -> m b -> m b
a >> b = a >>= f
where f x = b
En Haskell, también se necesita un operador de fail
extra, pero esto es realmente un truco (y podría ser eliminado de Monad
en el futuro ).
Esta es la definición de Haskell de una Monad
:
class Monad m where
return :: m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b -- can be derives from (>>=)
fail :: String -> m a -- mostly a hack
Las acciones son de primera clase
Una gran cosa sobre las mónadas es que las acciones son de primera clase. Puede tomarlos en una variable, puede definir la función que toma acciones como entrada y produce otras acciones como salida. Por ejemplo, podemos definir un operador while
:
-- while x y : does action y while action x output True
while :: (Monad m) => m Boolean -> m a -> m ()
while x y = x >>= f
where f True = y >> while x y
f False = return ()
Resumen
Una Monad
es un conjunto de acciones en algún dominio. La mónada / dominio define el tipo de "efectos" que son posibles. Los operadores >>
y >>=
representan la secuencia de acciones y la expresión monádica se puede utilizar para representar cualquier tipo de "subprograma (imperativo)" en su programa (funcional) Haskell.
Las grandes cosas son eso:
puede diseñar su propia
Monad
que sea compatible con las características y los efectos que deseeconsulte
Prompt
un ejemplo de un "subprograma solo de diálogo",vea
Rand
para un ejemplo de "muestreo solo del subprograma";
puede escribir sus propias estructuras de control (
while
,throw
,catch
o más exóticas) como funciones que toman acciones y las componen de alguna manera para producir macro-acciones más grandes.
MonadRandom
Una buena forma de entender las mónadas es el paquete MonadRandom
. La mónada Rand
está compuesta de acciones cuyo resultado puede ser aleatorio (el efecto es aleatoriedad). Una acción en esta mónada es algún tipo de variable aleatoria (o más exactamente un proceso de muestreo):
-- Sample an Int from some distribution
action :: Rand Int
Usar Rand
para hacer algunos algoritmos de muestreo / aleatorios es bastante interesante porque tiene variables aleatorias como valores de primera clase:
-- Estimate mean by sampling nsamples times the random variable x
sampleMean :: Real a => Int -> m a -> m a
sampleMean n x = ...
En esta configuración, la función de sequence
de Prelude
,
sequence :: Monad m => [m a] -> m [a]
se convierte
sequence :: [Rand a] -> Rand [a]
Crea una variable aleatoria obtenida por muestreo independientemente de una lista de variables aleatorias.
Una cosa que a menudo me ayuda a comprender la naturaleza de algo es examinarlo de la manera más trivial posible. De esa forma, no me distraigo con conceptos potencialmente no relacionados. Teniendo esto en cuenta, creo que puede ser útil comprender la naturaleza de Identity Monad , ya que es la implementación más trivial posible de una Monad (creo).
¿Qué es lo interesante de Identity Monad? Creo que es que me permite expresar la idea de evaluar expresiones en un contexto definido por otras expresiones. Y para mí, esa es la esencia de cada Mónada que he encontrado (hasta ahora).
Si ya has tenido mucha exposición a los lenguajes de programación "mainstream" antes de aprender Haskell (como yo lo hice), entonces esto no parece muy interesante en absoluto. Después de todo, en un lenguaje de programación convencional, las sentencias se ejecutan en secuencia, una después de la otra (excepto las construcciones de flujo de control, por supuesto). Y, naturalmente, podemos suponer que cada afirmación se evalúa en el contexto de todas las declaraciones ejecutadas previamente y que esas declaraciones ejecutadas previamente pueden alterar el entorno y el comportamiento de la instrucción que se está ejecutando actualmente.
Todo eso es más o menos un concepto extraño en un lenguaje funcional y perezoso como Haskell. El orden en que se evalúan los cálculos en Haskell está bien definido, pero a veces es difícil de predecir, y aún más difícil de controlar. Y para muchos tipos de problemas, eso está bien. Pero otros tipos de problemas (por ejemplo, IO) son difíciles de resolver sin alguna forma conveniente de establecer un orden implícito y el contexto entre los cálculos en su programa.
En cuanto a los efectos secundarios, específicamente, a menudo se pueden transformar (a través de una Mónada) en un simple paso de estado, que es perfectamente legal en un lenguaje funcional puro. Algunas Mónadas no parecen ser de esa naturaleza, sin embargo. Las mónadas como la IO Monad o la mónada ST literalmente realizan acciones de efecto secundario. Hay muchas maneras de pensar sobre esto, pero una forma de pensarlo es que solo porque mis cálculos deben existir en un mundo sin efectos secundarios, la Mónada puede que no. Como tal, la Mónada es libre de establecer un contexto para que se ejecute mi cálculo que se basa en los efectos secundarios definidos por otros cálculos.
Finalmente, debo negar que definitivamente no soy un experto en Haskell. Como tal, entiendan que todo lo que he dicho es más o menos lo que yo pienso sobre este tema y es posible que los repudie más adelante cuando entienda más a las Mónadas.