Las vistas 1 y 2 son incorrectas en general.
- Cualquier tipo de datos
* -> *
puede funcionar como una etiqueta, las mónadas son mucho más que eso.
- (Con la excepción de la
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 valor x :: 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 tenemos f :: a -> b
y g :: b -> c
obtenemos g . f :: a -> c
. Tenga en cuenta que esto también funciona para nuestros valores convertidos: si lo tenemos x :: a
y lo convertimos en nuestra representación, obtenemos f . ((\_ -> x) :: () -> a) :: () -> b
.
Esta representación tiene algunas propiedades muy importantes, a saber:
- Tenemos una función muy especial: la función de identidad
id :: a -> a
para cada tipo a
. Es un elemento de identidad con respecto a .
: f
es igual a f . id
y a id . f
.
- El operador de composición de funciones
.
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 tipo m :: * -> *
. (En Haskell, m
es []
, IO
, Maybe
, etc.) y la categoría voluntad contiene todas las funciones de tipos a -> 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:
- Tener un operador (llamémoslo
<=<
) que compone funciones f :: a -> m b
y g :: b -> m c
en algo como g <=< f :: a -> m c
. Y, debe ser asociativo.
- Para tener alguna función de identidad para cada tipo, llamémosla
return
. También queremos que f <=< return
sea lo mismo f
y lo mismo que return <=< f
.
Cualquiera m :: * -> *
para el que tenemos tales funciones return
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 por m
.
(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 tipos m a
y componimos funciones de tipos a -> 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 usando return
y >>=
se llaman las leyes de mónada .
Un ejemplo: listas
Si elegimos m
ser []
, obtenemos una categoría de funciones de tipos a -> [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ón f :: a -> [b]
y g :: b -> [c]
funciona de la siguiente manera: g <=< f :: a -> [c]
significa calcular todos los resultados de tipo posibles [b]
, aplicarlos g
a cada uno de ellos y recopilar todos los resultados en una sola lista. Expresado en Haskell
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
o usando >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Tenga en cuenta que, en este ejemplo, los tipos de retorno eran [a]
tan posibles que no contenían ningún valor de tipo a
. 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 gusta IO
o State
), pero otras no, me gusta []
o Maybe
.
La mónada IO
Como mencioné, la IO
mónada es algo especial. Un valor de tipo IO a
significa un valor de tipo a
construido al interactuar con el entorno del programa. Entonces (a diferencia de todas las otras mónadas), no podemos describir un valor de tipo IO 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:
- Composición
f :: a -> IO b
y g :: b -> IO c
medios: Computación f
que interactúa con el entorno, y luego computación g
que utiliza el valor y calcula el resultado interactuando con el entorno.
return
simplemente agrega la IO
"etiqueta" al valor (simplemente "calculamos" el resultado manteniendo el entorno intacto).
- Las leyes de mónada (asociatividad, identidad) están garantizadas por el compilador.
Algunas notas:
- Dado que los cálculos monádicos siempre tienen el tipo de resultado
m a
, no hay forma de "escapar" de la IO
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.
- Cuando un programador funcional no sabe cómo hacer algo de una manera pura, puede (como último recurso) programar la tarea mediante algún cálculo con estado dentro de la
IO
mónada. Esta es la razón por la cual a IO
menudo se le llama bin bin de programador .
- Tenga en cuenta que en un mundo impuro (en el sentido de la programación funcional) leer un valor también puede cambiar el entorno (como consumir la entrada del usuario). Es por eso que funciones como
getChar
deben tener un tipo de resultado de IO something
.