En mi humilde opinión, las respuestas a la famosa pregunta "¿Qué es una mónada?" , especialmente los más votados, trate de explicar qué es una mónada sin explicar claramente por qué las mónadas son realmente necesarias . ¿Pueden explicarse como la solución a un problema?
366
Respuestas:
¿Por qué necesitamos mónadas?
Entonces, tenemos un primer gran problema. Este es un programa:
f(x) = 2 * x
g(x,y) = x / y
¿Cómo podemos decir qué se debe ejecutar primero ? ¿Cómo podemos formar una secuencia ordenada de funciones (es decir, un programa ) usando no más que funciones ?
Solución: componer funciones . Si quieres primero
g
y luegof
, solo escribef(g(x,y))
. De esta manera, "el programa" es una función, así:main = f(g(x,y))
. Bien pero ...Más problemas: algunas funciones pueden fallar (es decir
g(2,0)
, dividir por 0). No tenemos "excepciones" en FP (una excepción no es una función). ¿Como lo resolvemos?Solución: Permitamos que las funciones devuelvan dos tipos de cosas : en lugar de tener
g : Real,Real -> Real
(función de dos reales en real), permitamosg : Real,Real -> Real | Nothing
(función de dos reales en (real o nada)).Pero las funciones deberían (para ser más simples) devolver solo una cosa .
Solución: creemos un nuevo tipo de datos para devolver, un " tipo de boxeo " que encierra tal vez un real o simplemente nada. Por lo tanto, podemos tener
g : Real,Real -> Maybe Real
. Bien pero ...¿Qué pasa ahora con
f(g(x,y))
?f
no está listo para consumir aMaybe Real
. Y no queremos cambiar todas las funciones con las que podríamos conectarnosg
para consumir aMaybe Real
.Solución: tengamos una función especial para "conectar" / "componer" / "vincular" funciones . De esa manera, podemos, detrás de escena, adaptar la salida de una función para alimentar la siguiente.
En nuestro caso:
g >>= f
(conectar / componerg
af
). Queremos>>=
obtenerg
el resultado, inspeccionarlo y, en caso de que seaNothing
, no llamef
ni regreseNothing
; o por el contrario, extraiga el cuadroReal
y alimentef
con él. (Este algoritmo es solo la implementación de>>=
para elMaybe
tipo). También tenga en cuenta que>>=
debe escribirse solo una vez por "tipo de boxeo" (cuadro diferente, algoritmo de adaptación diferente).Surgen muchos otros problemas que pueden resolverse usando este mismo patrón: 1. Use una "caja" para codificar / almacenar diferentes significados / valores, y tener funciones como
g
esa devolver esos "valores en caja". 2. Tenga un compositor / enlazadorg >>= f
para ayudar a conectarg
la salida def
la entrada, de modo que no tengamos que cambiar nadaf
.Los problemas notables que se pueden resolver con esta técnica son:
teniendo un estado global que cada función en la secuencia de funciones ("el programa") puede compartir: solución
StateMonad
.No nos gustan las "funciones impuras": funciones que producen diferentes resultados para la misma entrada. Por lo tanto, marquemos esas funciones, haciendo que devuelvan un valor etiquetado / encuadrado:
IO
mónada.Felicidad total!
fuente
IO
mónada es solo un problema más en la listaIO
(punto 7). Por otro lado,IO
solo aparece una vez y al final, por lo tanto, no entiendo tu "la mayor parte del tiempo hablando ... sobre IO".Either
). La mayor parte de la respuesta es sobre "¿Por qué necesitamos functors?".g >>= f
para ayudar a conectarg
la salida af
la entrada, para que no tengamos que cambiar nadaf
". Esto no es correcto en absoluto. Antes, enf(g(x,y))
,f
podía producir cualquier cosa. Podría serf:: Real -> String
. Con "composición monádica" se debe cambiar para producirMaybe String
, o de lo contrario los tipos no encajarán. ¡Además, en>>=
sí mismo no encaja ! Es lo>=>
que hace esta composición, no>>=
. Vea la discusión con dfeuer bajo la respuesta de Carl.La respuesta es, por supuesto, "No lo hacemos" . Como con todas las abstracciones, no es necesario.
Haskell no necesita una abstracción de mónada. No es necesario para realizar IO en un lenguaje puro. El
IO
tipo se encarga de eso por sí solo. El desugaring monádico existente dedo
bloques podría ser sustituido por desugaring abindIO
,returnIO
yfailIO
como se define en elGHC.Base
módulo. (No es un módulo documentado sobre piratería, por lo que tendré que señalar su fuente de documentación). Entonces, no, no hay necesidad de la abstracción de mónada.Entonces, si no es necesario, ¿por qué existe? Porque se descubrió que muchos patrones de cálculo forman estructuras monádicas. La abstracción de una estructura permite escribir código que funciona en todas las instancias de esa estructura. Para decirlo de manera más concisa: reutilización de código.
En lenguajes funcionales, la herramienta más poderosa encontrada para la reutilización de código ha sido la composición de funciones. El buen viejo
(.) :: (b -> c) -> (a -> b) -> (a -> c)
operador es extremadamente poderoso. Facilita la escritura de pequeñas funciones y las une con una mínima sobrecarga sintáctica o semántica.Pero hay casos en que los tipos no funcionan del todo bien. ¿Qué haces cuando tienes
foo :: (b -> Maybe c)
ybar :: (a -> Maybe b)
?foo . bar
no escribe, porqueb
yMaybe b
no son del mismo tipo.Pero ... es casi correcto. Solo quieres un poco de libertad. Desea poder tratar
Maybe b
como si fuera básicamenteb
. Sin embargo, es una mala idea simplemente tratarlos como si fueran del mismo tipo. Eso es más o menos lo mismo que los punteros nulos, que Tony Hoare llamó el error de mil millones de dólares . Entonces, si no puede tratarlos como el mismo tipo, tal vez pueda encontrar una manera de extender el mecanismo de composición que(.)
proporciona.En ese caso, es importante examinar realmente la teoría subyacente
(.)
. Afortunadamente, alguien ya ha hecho esto por nosotros. Resulta que la combinación de(.)
yid
forma una construcción matemática conocida como categoría . Pero hay otras formas de formar categorías. Una categoría de Kleisli, por ejemplo, permite que los objetos que se componen se aumenten un poco. Una categoría de KleisliMaybe
consistiría en(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
yid :: a -> Maybe a
. Es decir, los objetos en la categoría aumentan(->)
con aMaybe
, por lo que se(a -> b)
convierte(a -> Maybe b)
.Y de repente, hemos extendido el poder de la composición a cosas en las que la
(.)
operación tradicional no funciona. Esta es una fuente de nuevo poder de abstracción. Las categorías de Kleisli funcionan con más tipos que simplementeMaybe
. Trabajan con todo tipo que pueda ensamblar una categoría adecuada, obedeciendo las leyes de categoría.id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
Siempre que pueda probar que su tipo obedece esas tres leyes, puede convertirlo en una categoría Kleisli. ¿Y cuál es el problema con eso? Bueno, resulta que las mónadas son exactamente lo mismo que las categorías de Kleisli.
Monad
'sreturn
es lo mismo que Kleisliid
.Monad
's(>>=)
no es idéntico a Kleisli(.)
, pero resulta muy fácil escribir cada uno en términos del otro. Y las leyes de categoría son las mismas que las leyes de mónada, cuando las traduces a través de la diferencia entre(>>=)
y(.)
.Entonces, ¿por qué pasar por toda esta molestia? ¿Por qué tener una
Monad
abstracción en el lenguaje? Como mencioné anteriormente, permite la reutilización del código. Incluso permite la reutilización de código a lo largo de dos dimensiones diferentes.La primera dimensión de la reutilización del código proviene directamente de la presencia de la abstracción. Puede escribir código que funcione en todas las instancias de la abstracción. Existe todo el paquete de mónada-bucles que consta de bucles que funcionan con cualquier instancia de
Monad
.La segunda dimensión es indirecta, pero se sigue de la existencia de la composición. Cuando la composición es fácil, es natural escribir código en fragmentos pequeños y reutilizables. De la misma manera, tener el
(.)
operador para funciones fomenta la escritura de funciones pequeñas y reutilizables.Entonces, ¿por qué existe la abstracción? Porque se ha demostrado que es una herramienta que permite una mayor composición en el código, lo que resulta en la creación de código reutilizable y fomenta la creación de más código reutilizable. La reutilización de código es uno de los santos griales de la programación. La abstracción de la mónada existe porque nos mueve un poco hacia ese santo grial.
fuente
newtype Kleisli m a b = Kleisli (a -> m b)
. Las categorías de Kleisli son funciones en las que el tipo de retorno categórico (b
en este caso) es el argumento para un constructor de tiposm
. IffKleisli m
forma una categoría,m
es una mónada.Kleisli m
parece formar una categoría cuyos objetos son tipos de Haskell y tales que las flechas dea
ab
son las funciones dea
am b
, conid = return
y(.) = (<=<)
. ¿Es correcto, o estoy mezclando diferentes niveles de cosas o algo?a
yb
, pero no son funciones simples. Están decoradas con un extram
en el valor de retorno de la función.Benjamin Pierce dijo en TAPL
Es por eso que un lenguaje equipado con un poderoso sistema de tipos es estrictamente más expresivo que un lenguaje mal escrito. Puedes pensar en las mónadas de la misma manera.
Como @Carl y sigfpe point, puede equipar un tipo de datos con todas las operaciones que desee sin recurrir a mónadas, clases de tipos o cualquier otra cosa abstracta. Sin embargo, las mónadas le permiten no solo escribir código reutilizable, sino también abstraer todos los detalles redundantes.
Como ejemplo, digamos que queremos filtrar una lista. La forma más simple es usar el
filter
función:,filter (> 3) [1..10]
que es igual[4,5,6,7,8,9,10]
.Una versión un poco más complicada de
filter
, que también pasa un acumulador de izquierda a derecha, esPara obtener todo
i
, de modo quei <= 10, sum [1..i] > 4, sum [1..i] < 25
podamos escribirque es igual
[3,4,5,6]
.O podemos redefinir la
nub
función, que elimina elementos duplicados de una lista, en términos defilterAccum
:nub' [1,2,4,5,4,3,1,8,9,4]
es igual[1,2,4,5,3,8,9]
. Aquí se pasa una lista como acumulador. El código funciona, porque es posible salir de la mónada de la lista, por lo que todo el cómputo se mantiene puro (en realidadnotElem
no se usa>>=
, pero podría). Sin embargo, no es posible abandonar con seguridad la mónada IO (es decir, no puede ejecutar una acción IO y devolver un valor puro; el valor siempre estará envuelto en la mónada IO). Otro ejemplo son las matrices mutables: después de haber dejado la mónada ST, donde vive una matriz mutable, ya no puede actualizar la matriz en tiempo constante. Entonces necesitamos un filtro monádico delControl.Monad
módulo:filterM
ejecuta una acción monádica para todos los elementos de una lista, produciendo elementos, para los cuales la acción monádica regresaTrue
.Un ejemplo de filtrado con una matriz:
imprime
[1,2,4,5,3,8,9]
como se esperaba.Y una versión con la mónada IO, que pregunta qué elementos devolver:
P.ej
Y como ilustración final,
filterAccum
se puede definir en términos defilterM
:con el
StateT
mónada, que se usa debajo del capó, siendo solo un tipo de datos ordinario.Este ejemplo ilustra que las mónadas no solo le permiten abstraer el contexto computacional y escribir código reutilizable limpio (debido a la componibilidad de las mónadas, como explica @Carl), sino también tratar los tipos de datos definidos por el usuario y las primitivas integradas de manera uniforme.
fuente
No creo
IO
que deba verse como una mónada particularmente sobresaliente, pero ciertamente es una de las más asombrosas para principiantes, así que la usaré para mi explicación.Construyendo ingenuamente un sistema IO para Haskell
El sistema IO más simple concebible para un lenguaje puramente funcional (y de hecho con el que Haskell comenzó) es este:
Con pereza, esa firma simple es suficiente para construir programas de terminal interactivos , aunque muy limitada. Lo más frustrante es que solo podemos generar texto. ¿Qué pasa si agregamos algunas posibilidades de salida más interesantes?
lindo, pero por supuesto una "salida alternativa" mucho más realista sería escribir en un archivo . Pero entonces también querrías alguna forma de leer archivos. ¿Cualquier oportunidad?
Bueno, cuando tomamos nuestro
main₁
programa y simplemente canalizamos un archivo al proceso (usando las instalaciones del sistema operativo), esencialmente hemos implementado la lectura de archivos. Si pudiéramos activar esa lectura de archivos desde el lenguaje Haskell ...Esto usaría un "programa interactivo"
String->[Output]
, lo alimentaría con una cadena obtenida de un archivo y generaría un programa no interactivo que simplemente ejecuta el dado.Aquí hay un problema: realmente no tenemos una noción de cuándo se lee el archivo. La
[Output]
lista ciertamente da un buen orden a las salidas , pero no obtenemos un orden de cuándo se realizarán las entradas .Solución: haga que los eventos de entrada también sean elementos en la lista de cosas que hacer.
Bien, ahora puede detectar un desequilibrio: puede leer un archivo y hacer que la salida dependa de él, pero no puede usar el contenido del archivo para decidir, por ejemplo, también leer otro archivo. Solución obvia: hacer que el resultado de los eventos de entrada también sea algo de tipo
IO
, no soloOutput
. Eso seguro incluye una salida de texto simple, pero también permite leer archivos adicionales, etc.Eso ahora le permitiría expresar cualquier operación de archivo que desee en un programa (aunque tal vez no con un buen rendimiento), pero es algo complicado:
main₃
produce una lista completa de acciones. ¿Por qué no usamos simplemente la firma:: IO₁
, que tiene esto como un caso especial?Las listas ya no dan una descripción confiable del flujo del programa: la mayoría de los cálculos posteriores solo serán "anunciados" como resultado de alguna operación de entrada. Por lo tanto, podríamos deshacernos de la estructura de la lista, y simplemente contrarrestar un "y luego hacer" a cada operación de salida.
¡No está mal!
Entonces, ¿qué tiene todo esto que ver con las mónadas?
En la práctica, no querrás usar constructores simples para definir todos tus programas. Tendría que haber un buen par de constructores tan fundamentales, pero para la mayoría de las cosas de nivel superior nos gustaría escribir una función con alguna buena firma de alto nivel. Resulta que la mayoría de estos se verían bastante similares: acepte algún tipo de valor con tipo significativo y produzca una acción IO como resultado.
Evidentemente hay un patrón aquí, y será mejor que lo escribamos como
Ahora eso comienza a parecer familiar, pero todavía estamos lidiando con funciones simples poco disfrazadas bajo el capó, y eso es arriesgado: cada "acción de valor" tiene la responsabilidad de transmitir la acción resultante de cualquier función contenida (de lo contrario el flujo de control de todo el programa se ve fácilmente interrumpido por una acción de mal comportamiento en el medio). Será mejor que expliquemos ese requisito. Bueno, resulta que esas son las leyes de mónada , aunque no estoy seguro de que realmente podamos formularlas sin los operadores de enlace / unión estándar.
En cualquier caso, ahora hemos alcanzado una formulación de IO que tiene una instancia de mónada adecuada:
Obviamente, esta no es una implementación eficiente de IO, pero en principio es utilizable.
fuente
IO3 a ≡ Cont IO2 a
. Pero quise decir ese comentario más como un guiño a aquellos que ya conocen la continuación de la mónada, ya que no tiene exactamente la reputación de ser amigable para principiantes.Las mónadas son solo un marco conveniente para resolver una clase de problemas recurrentes. Primero, las mónadas deben ser functores (es decir, deben admitir la asignación sin mirar los elementos (o su tipo)), también deben traer una operación de enlace (o encadenamiento) y una forma de crear un valor monádico a partir de un tipo de elemento (
return
). Finalmente,bind
yreturn
debe satisfacer dos ecuaciones (identidades izquierda y derecha), también llamadas leyes de mónada. (Alternativamente, uno podría definir mónadas para tener unflattening operation
lugar de vinculante).La lista mónada se usa comúnmente para tratar el no determinismo. La operación de vinculación selecciona un elemento de la lista (intuitivamente todos en mundos paralelos ), le permite al programador hacer algunos cálculos con ellos y luego combina los resultados en todos los mundos en una sola lista (concatenando o aplanar una lista anidada) ) Así es como se definiría una función de permutación en el marco monádico de Haskell:
Este es un ejemplo repl sesión:
Cabe señalar que la mónada de la lista no es de ninguna manera un cómputo de efectos secundarios. Una estructura matemática que es una mónada (es decir, conforme a las interfaces y leyes mencionadas anteriormente) no implica efectos secundarios, aunque los fenómenos de efectos secundarios a menudo encajan muy bien en el marco monádico.
fuente
Las mónadas sirven básicamente para componer funciones juntas en una cadena. Período.
Ahora, la forma en que se componen difiere entre las mónadas existentes, lo que resulta en diferentes comportamientos (por ejemplo, para simular un estado mutable en la mónada estatal).
La confusión acerca de las mónadas es que, al ser tan generales, es decir, un mecanismo para componer funciones, se pueden usar para muchas cosas, lo que lleva a las personas a creer que las mónadas son sobre el estado, sobre IO, etc., cuando solo se trata de "componer funciones ".
Ahora, una cosa interesante sobre las mónadas, es que el resultado de la composición siempre es del tipo "M a", es decir, un valor dentro de un sobre etiquetado con "M". Esta característica resulta realmente agradable de implementar, por ejemplo, una separación clara entre el código puro del impuro: declare todas las acciones impuras como funciones de tipo "IO a" y no proporcione ninguna función, al definir la mónada IO, para eliminar el " un "valor desde dentro del" IO a ". El resultado es que ninguna función puede ser pura y al mismo tiempo extraer un valor de un "IO a", porque no hay forma de tomar dicho valor mientras se mantiene puro (la función debe estar dentro de la mónada "IO" para usar tal valor). (NOTA: bueno, nada es perfecto, por lo que la "camisa de fuerza IO" se puede romper usando "unsafePerformIO: IO a -> a"
fuente
Necesita mónadas si tiene un constructor de tipos y funciones que devuelven valores de esa familia de tipos . Eventualmente, le gustaría combinar este tipo de funciones juntas . Estos son los tres elementos clave para responder por qué .
Déjame elaborar. Tienes
Int
,String
yReal
funciones de tipoInt -> String
,String -> Real
y así sucesivamente. Puede combinar estas funciones fácilmente, terminando conInt -> Real
. La vida es buena.Entonces, un día, necesitas crear una nueva familia de tipos . Podría deberse a que debe considerar la posibilidad de no devolver ningún valor (
Maybe
), devolver un error (Either
), múltiples resultados (List
), etc.Tenga en cuenta que
Maybe
es un constructor de tipos. Toma un tipo, me gustaInt
y devuelve un nuevo tipoMaybe Int
. Lo primero que debe recordar, sin constructor de tipo, sin mónada.Por supuesto, desea utilizar su constructor de tipos en su código, y pronto terminará con funciones como
Int -> Maybe String
yString -> Maybe Float
. Ahora, no puede combinar fácilmente sus funciones. La vida ya no es buena.Y aquí es cuando las mónadas vienen al rescate. Le permiten combinar ese tipo de funciones nuevamente. Solo necesitas cambiar la composición . para > == .
fuente