Mientras aprendía Haskell, me enfrenté a muchos tutoriales que intentaban explicar qué son las mónadas y por qué las mónadas son importantes en Haskell. Cada uno de ellos utilizó analogías, por lo que sería más fácil captar el significado. Al final del día, he terminado con 3 vistas diferentes de lo que es una mónada:
Vista 1: Mónada como etiqueta
A veces pienso que una mónada como etiqueta para un tipo específico. Por ejemplo, una función de tipo:
myfunction :: IO Int
myfunction es una función que cada vez que se realiza producirá un valor Int. El tipo de resultado no es Int sino IO Int. Entonces, IO es una etiqueta del valor Int que advierte al usuario que debe saber que el valor Int es el resultado de un proceso en el que se realizó una acción IO.
En consecuencia, este valor Int se ha marcado como valor que proviene de un proceso con IO, por lo tanto, este valor está "sucio". Tu proceso ya no es puro.
Vista 2: Monad como un espacio privado donde pueden suceder cosas desagradables.
En un sistema donde todo el proceso es puro y estricto, a veces es necesario tener efectos secundarios. Entonces, una mónada es solo un pequeño espacio que te permite hacer efectos secundarios desagradables. En este espacio, puedes escapar del mundo puro, volverse impuro, hacer tu proceso y luego volver con un valor.
Vista 3: Mónada como en teoría de categoría
Esta es la opinión que no entiendo completamente. Una mónada es solo un functor de la misma categoría o subcategoría. Por ejemplo, tiene los valores Int y como subcategoría IO Int, que son los valores Int generados después de un proceso IO.
¿Son correctas estas opiniones? ¿Cuál es más preciso?
Respuestas:
Las vistas 1 y 2 son incorrectas en general.
* -> *
puede funcionar como una etiqueta, las mónadas son mucho más que eso.IO
mónada) los cálculos dentro de una mónada no son impuros. Simplemente representan cálculos que percibimos que tienen efectos secundarios, pero son puros.Ambos malentendidos provienen de centrarse en la
IO
mónada, que en realidad es un poco especial.Intentaré elaborar un poco el # 3, sin entrar en la teoría de la categoría si es posible.
Cálculos estándar
Todos los cálculos en un lenguaje de programación funcional se pueden ver como funciona con un tipo de fuente y un tipo de destino:
f :: a -> b
. Si una función tiene más de un argumento, podemos convertirla en una función de un argumento al cursar (ver también wiki de Haskell ). Y si tenemos sólo un valorx :: a
(una función con argumentos 0), podemos convertirlo en una función que toma un argumento del tipo de unidad :(\_ -> x) :: () -> a
.Podemos construir programas más complejos a partir de otros más simples componiendo tales funciones usando el
.
operador. Por ejemplo, si tenemosf :: a -> b
yg :: b -> c
obtenemosg . f :: a -> c
. Tenga en cuenta que esto también funciona para nuestros valores convertidos: si lo tenemosx :: a
y lo convertimos en nuestra representación, obtenemosf . ((\_ -> x) :: () -> a) :: () -> b
.Esta representación tiene algunas propiedades muy importantes, a saber:
id :: a -> a
para cada tipoa
. Es un elemento de identidad con respecto a.
:f
es igual af . id
y aid . f
..
es asociativo .Cálculos monádicos
Supongamos que queremos seleccionar y trabajar con alguna categoría especial de cálculos, cuyo resultado contiene algo más que el valor de retorno único. No queremos especificar qué significa "algo más", queremos mantener las cosas lo más generales posible. La forma más general de representar "algo más" es representarlo como una función de tipo, un tipo
m
de tipo* -> *
(es decir, convierte un tipo en otro). Entonces, para cada categoría de cálculos con los que queremos trabajar, tendremos alguna función de tipom :: * -> *
. (En Haskell,m
es[]
,IO
,Maybe
, etc.) y la categoría voluntad contiene todas las funciones de tiposa -> m b
.Ahora nos gustaría trabajar con las funciones en dicha categoría de la misma manera que en el caso básico. Queremos poder componer estas funciones, queremos que la composición sea asociativa y queremos tener una identidad. Necesitamos:
<=<
) que compone funcionesf :: a -> m b
yg :: b -> m c
en algo comog <=< f :: a -> m c
. Y, debe ser asociativo.return
. También queremos quef <=< return
sea lo mismof
y lo mismo quereturn <=< f
.Cualquiera
m :: * -> *
para el que tenemos tales funcionesreturn
y<=<
se llama mónada . Nos permite crear cálculos complejos a partir de los más simples, como en el caso básico, pero ahora los tipos de valores de retorno se transforman porm
.(En realidad, abusé un poco del término categoría aquí. En el sentido de teoría de categorías, podemos llamar a nuestra construcción una categoría solo después de saber que obedece estas leyes).
Mónadas en Haskell
En Haskell (y otros lenguajes funcionales) trabajamos principalmente con valores, no con funciones de tipos
() -> a
. Entonces, en lugar de definir<=<
para cada mónada, definimos una función(>>=) :: m a -> (a -> m b) -> m b
. Dicha definición alternativa es equivalente, podemos expresarla>>=
usando<=<
y viceversa (intente como ejercicio o vea las fuentes ). El principio es menos obvio ahora, pero sigue siendo el mismo: nuestros resultados son siempre de tiposm a
y componimos funciones de tiposa -> m b
.Para cada mónada que creamos, no debemos olvidar verificar eso
return
y<=<
tener las propiedades que requerimos: asociatividad e identidad izquierda / derecha. Expresado usandoreturn
y>>=
se llaman las leyes de mónada .Un ejemplo: listas
Si elegimos
m
ser[]
, obtenemos una categoría de funciones de tiposa -> [b]
. Dichas funciones representan cálculos no deterministas, cuyos resultados podrían ser uno o más valores, pero tampoco valores. Esto da lugar a la llamada lista mónada . La composiciónf :: a -> [b]
yg :: b -> [c]
funciona de la siguiente manera:g <=< f :: a -> [c]
significa calcular todos los resultados de tipo posibles[b]
, aplicarlosg
a cada uno de ellos y recopilar todos los resultados en una sola lista. Expresado en Haskello usando
>>=
Tenga en cuenta que, en este ejemplo, los tipos de retorno eran
[a]
tan posibles que no contenían ningún valor de tipoa
. De hecho, no existe un requisito para una mónada de que el tipo de retorno debe tener tales valores. Algunas mónadas siempre tienen (me gustaIO
oState
), pero otras no, me gusta[]
oMaybe
.La mónada IO
Como mencioné, la
IO
mónada es algo especial. Un valor de tipoIO a
significa un valor de tipoa
construido al interactuar con el entorno del programa. Entonces (a diferencia de todas las otras mónadas), no podemos describir un valor de tipoIO a
usando alguna construcción pura. AquíIO
hay simplemente una etiqueta o una etiqueta que distingue los cálculos que interactúan con el entorno. Este es (el único caso) donde las vistas # 1 y # 2 son correctas.Para la
IO
mónada:f :: a -> IO b
yg :: b -> IO c
medios: Computaciónf
que interactúa con el entorno, y luego computacióng
que utiliza el valor y calcula el resultado interactuando con el entorno.return
simplemente agrega laIO
"etiqueta" al valor (simplemente "calculamos" el resultado manteniendo el entorno intacto).Algunas notas:
m a
, no hay forma de "escapar" de laIO
mónada. El significado es: una vez que una computación interactúa con el entorno, no se puede construir una computación que no lo haga.IO
mónada. Esta es la razón por la cual aIO
menudo se le llama bin bin de programador .getChar
deben tener un tipo de resultado deIO something
.fuente
IO
no tiene una semántica especial desde el punto de vista del lenguaje. Es no especial, se comporta como cualquier otro código. Solo la implementación de la biblioteca en tiempo de ejecución es especial. Además, hay una forma especial de escapar (unsafePerformIO
). Creo que esto es importante porque la gente suele pensarIO
en un elemento de lenguaje especial o una etiqueta declarativa. No lo es.coerce :: a -> b
que convierta dos tipos (y bloquee su programa en la mayoría de los casos). Vea este ejemplo : puede convertir incluso una función enInt
etc.runST :: (forall s. GHC.ST.ST s a) -> a
Vista 1: Mónada como etiqueta
"En consecuencia, este valor Int se ha marcado como valor que proviene de un proceso con IO, por lo tanto, este valor es" sucio "".
"IO Int" no es en general un valor Int (aunque puede ser en algunos casos como "return 3"). Es un procedimiento que genera algún valor Int. Las diferentes ejecuciones de este "procedimiento" pueden producir diferentes valores Int.
Una mónada m es un "lenguaje de programación" incrustado (imperativo): dentro de este lenguaje es posible definir algunos "procedimientos". Un valor monádico (de tipo ma) es un procedimiento en este "lenguaje de programación" que genera un valor de tipo a.
Por ejemplo:
es un procedimiento que genera un valor de tipo Int.
Luego:
es un procedimiento que genera dos (posiblemente diferentes) ent.
Cada "lenguaje" de este tipo admite algunas operaciones:
dos procedimientos (ma y mb) pueden "concatenarse": puede crear un procedimiento más grande (ma >> mb) compuesto por el primero y luego por el segundo;
además, la salida (a) del primero puede afectar al segundo (ma >> = \ a -> ...);
un procedimiento (retorno x) puede producir algún valor constante (x).
Los diferentes lenguajes de programación integrados difieren en las cosas amables que admiten, como:
fuente
No confunda un tipo monádico con la clase mónada.
Un tipo monádico (es decir, un tipo que es una instancia de la clase mónada) resolvería un problema particular (en principio, cada tipo monádico resuelve uno diferente): Estado, Aleatorio, Quizás, IO. Todos ellos son tipos con contexto (lo que llamas "etiqueta", pero eso no es lo que los hace ser una mónada).
Para todos ellos, existe la necesidad de "encadenar operaciones con opción" (una operación depende del resultado de la anterior). Aquí entra en juego la clase mónada: haga que su tipo (resolviendo un problema dado) sea una instancia de la clase mónada y se resuelva el problema de encadenamiento.
Ver ¿Qué resuelve la clase mónada?
fuente