¿Qué es una mónada?
Habiendo analizado brevemente Haskell recientemente, ¿cuál sería una explicación breve, sucinta y práctica de lo que es esencialmente una mónada?
He encontrado que la mayoría de las explicaciones que he encontrado son bastante inaccesibles y carecen de detalles prácticos.
Primero: el término mónada es un poco vago si no eres matemático. Un término alternativo es generador de cálculos , que describe un poco más para qué son realmente útiles.
Son un patrón para encadenar operaciones. Se parece un poco al encadenamiento de métodos en los lenguajes orientados a objetos, pero el mecanismo es ligeramente diferente.
El patrón se usa principalmente en lenguajes funcionales (especialmente Haskell, que usa mónadas de manera generalizada), pero se puede usar en cualquier lenguaje que admita funciones de orden superior (es decir, funciones que pueden tomar otras funciones como argumentos).
Las matrices en JavaScript admiten el patrón, así que usémoslo como primer ejemplo.
La esencia del patrón es que tenemos un tipo ( Array
en este caso) que tiene un método que toma una función como argumento. La operación proporcionada debe devolver una instancia del mismo tipo (es decir, devolver un Array
).
Primero, un ejemplo de encadenamiento de métodos que no utiliza el patrón de mónada:
[1,2,3].map(x => x + 1)
El resultado es [2,3,4]
. El código no se ajusta al patrón de mónada, ya que la función que proporcionamos como argumento devuelve un número, no una matriz. La misma lógica en forma de mónada sería:
[1,2,3].flatMap(x => [x + 1])
Aquí proporcionamos una operación que devuelve un Array
, por lo que ahora se ajusta al patrón. El flatMap
método ejecuta la función proporcionada para cada elemento de la matriz. Espera una matriz como resultado para cada invocación (en lugar de valores únicos), pero fusiona el conjunto de matrices resultante en una única matriz. Entonces el resultado final es el mismo, la matriz [2,3,4]
.
(El argumento de función proporcionado a un método como map
o flatMap
a menudo se denomina "devolución de llamada" en JavaScript. Lo llamaré "operación" ya que es más general).
Si encadenamos múltiples operaciones (de la forma tradicional):
[1,2,3].map(a => a + 1).filter(b => b != 3)
Resultados en la matriz[2,4]
El mismo encadenamiento en forma de mónada:
[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])
Produce el mismo resultado, la matriz [2,4]
.
¡Notarás inmediatamente que la forma mónada es bastante más fea que la no mónada! Esto simplemente demuestra que las mónadas no son necesariamente "buenas". Son un patrón que a veces es beneficioso y otras no.
Tenga en cuenta que el patrón de mónada se puede combinar de otra manera:
[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))
Aquí el enlace está anidado en lugar de encadenado, pero el resultado es el mismo. Ésta es una propiedad importante de las mónadas, como veremos más adelante. Esto significa que dos operaciones combinadas pueden tratarse igual que una sola operación.
La operación puede devolver una matriz con diferentes tipos de elementos, por ejemplo transformando una matriz de números en una matriz de cadenas o algo más; siempre que siga siendo una matriz.
Esto se puede describir de manera un poco más formal usando la notación mecanografiada. Una matriz tiene el tipo Array<T>
, donde T
es el tipo de los elementos de la matriz. El método flatMap()
toma un argumento de función del tipo T => Array<U>
y devuelve un archivo Array<U>
.
Generalizado, una mónada es cualquier tipo Foo<Bar>
que tenga un método "bind" que toma un argumento de función de tipo Bar => Foo<Baz>
y devuelve un archivo Foo<Baz>
.
Esto responde qué son las mónadas. El resto de esta respuesta intentará explicar mediante ejemplos por qué las mónadas pueden ser un patrón útil en un lenguaje como Haskell que tiene un buen soporte para ellas.
Haskell y notación Do
Para traducir el ejemplo de mapa/filtro directamente a Haskell, reemplazamos flatMap
con el >>=
operador:
[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b]
El >>=
operador es la función de enlace en Haskell. Hace lo mismo que flatMap
en JavaScript cuando el operando es una lista, pero está sobrecargado con significados diferentes para otros tipos.
Pero Haskell también tiene una sintaxis dedicada para expresiones de mónada, el do
bloque -, que oculta por completo el operador de vinculación:
do
a <- [1,2,3]
b <- [a+1]
if b == 3 then [] else [b]
Esto oculta la "plomería" y le permite centrarse en las operaciones reales aplicadas en cada paso.
En un do
bloque, cada línea es una operación. La restricción aún mantiene que todas las operaciones en el bloque deben devolver el mismo tipo. Dado que la primera expresión es una lista, las otras operaciones también deben devolver una lista.
La flecha hacia atrás <-
parece engañosamente una asignación, pero tenga en cuenta que este es el parámetro pasado en el enlace. Entonces, cuando la expresión del lado derecho es una Lista de números enteros, la variable del lado izquierdo será un único número entero, pero se ejecutará para cada número entero de la lista.
Ejemplo: Navegación segura (del tipo Quizás)
Ya basta de listas, veamos cómo el patrón de mónada puede ser útil para otros tipos.
Es posible que algunas funciones no siempre devuelvan un valor válido. En Haskell esto está representado por el Maybe
tipo, que es una opción que es Just value
o Nothing
.
Por supuesto, encadenar operaciones que siempre devuelven un valor válido es sencillo:
streetName = getStreetName (getAddress (getUser 17))
Pero, ¿qué pasaría si alguna de las funciones pudiera regresar Nothing
? Necesitamos verificar cada resultado individualmente y solo pasar el valor a la siguiente función si no es así Nothing
:
case getUser 17 of
Nothing -> Nothing
Just user ->
case getAddress user of
Nothing -> Nothing
Just address ->
getStreetName address
¡Muchos controles repetitivos! Imagínese si la cadena fuera más larga. Haskell resuelve esto con el patrón de mónada para Maybe
:
do
user <- getUser 17
addr <- getAddress user
getStreetName addr
Este do
bloque invoca la función de vinculación para el Maybe
tipo (ya que el resultado de la primera expresión es a Maybe
). La función de vinculación solo ejecuta la siguiente operación si el valor es Just value
; de lo contrario, simplemente pasa la Nothing
siguiente operación.
Aquí se utiliza el patrón de mónada para evitar código repetitivo. Esto es similar a cómo otros lenguajes usan macros para simplificar la sintaxis, aunque las macros logran el mismo objetivo de una manera muy diferente.
Tenga en cuenta que es la combinación del patrón de mónada y la sintaxis compatible con mónadas en Haskell lo que da como resultado un código más limpio. En un lenguaje como JavaScript sin soporte de sintaxis especial para mónadas, dudo que el patrón de mónada pueda simplificar el código en este caso.
Estado mutable
Haskell no admite estados mutables. Todas las variables son constantes y todos los valores inmutables. Pero el State
tipo se puede utilizar para emular programación con estado mutable:
add2 :: State Integer Integer
add2 = do
-- add 1 to state
x <- get
put (x + 1)
-- increment in another way
modify (+1)
-- return state
get
evalState add2 7
=> 9
La add2
función construye una cadena de mónadas que luego se evalúa con 7 como estado inicial.
Obviamente esto es algo que sólo tiene sentido en Haskell. Otros idiomas admiten estados mutables desde el primer momento. Haskell generalmente "opta por" las características del lenguaje: usted habilita el estado mutable cuando lo necesita y el sistema de tipos garantiza que el efecto sea explícito. IO es otro ejemplo de esto.
OI
El IO
tipo se utiliza para encadenar y ejecutar funciones "impuras".
Como cualquier otro lenguaje práctico, Haskell tiene un montón de funciones integradas que interactúan con el mundo exterior putStrLine
: readLine
etc. Estas funciones se denominan "impuras" porque causan efectos secundarios o tienen resultados no deterministas. Incluso algo tan simple como obtener la hora se considera impuro porque el resultado no es determinista: llamarlo dos veces con los mismos argumentos puede devolver valores diferentes.
Una función pura es determinista: su resultado depende exclusivamente de los argumentos pasados y no tiene efectos secundarios en el medio ambiente además de devolver un valor.
Haskell fomenta fuertemente el uso de funciones puras; este es un importante punto de venta del lenguaje. Desafortunadamente para los puristas, se necesitan algunas funciones impuras para hacer algo útil. El compromiso de Haskell es separar limpiamente lo puro de lo impuro y garantizar que no haya forma de que funciones puras puedan ejecutar funciones impuras, directa o indirectamente.
Esto se garantiza dando el IO
tipo a todas las funciones impuras. El punto de entrada en el programa Haskell es la main
función que tiene el IO
tipo, por lo que podemos ejecutar funciones impuras en el nivel superior.
Pero, ¿cómo evita el lenguaje que funciones puras ejecuten funciones impuras? Esto se debe a la naturaleza perezosa de Haskell. Una función sólo se ejecuta si su salida es consumida por alguna otra función. Pero no hay forma de consumir un IO
valor excepto asignárselo main
. Entonces, si una función quiere ejecutar una función impura, debe estar conectada main
y tener el IO
tipo.
El uso del encadenamiento de mónadas para operaciones IO también garantiza que se ejecuten en un orden lineal y predecible, al igual que las declaraciones en un lenguaje imperativo.
Esto nos lleva al primer programa que la mayoría de la gente escribirá en Haskell:
main :: IO ()
main = do
putStrLn ”Hello World”
La do
palabra clave es superflua cuando solo hay una operación y, por lo tanto, no hay nada que vincular, pero la mantengo de todos modos por coherencia.
El ()
tipo significa "vacío". Este tipo de retorno especial solo es útil para funciones IO solicitadas por su efecto secundario.
Un ejemplo más largo:
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn ("hello" ++ name)
Esto crea una cadena de IO
operaciones y, dado que están asignadas a la main
función, se ejecutan.
La comparación IO
con Maybe
muestra la versatilidad del patrón de mónada. Para Maybe
, el patrón se utiliza para evitar código repetitivo moviendo la lógica condicional a la función de enlace. Para IO
, el patrón se utiliza para garantizar que todas las operaciones del IO
tipo estén secuenciadas y que IO
las operaciones no puedan "filtrarse" a funciones puras.
Resumiendo
En mi opinión subjetiva, el patrón de mónada sólo vale la pena en un lenguaje que tenga algún soporte incorporado para el patrón. De lo contrario, sólo conducirá a un código demasiado complicado. Pero Haskell (y algunos otros lenguajes) tienen algún soporte incorporado que oculta las partes tediosas, y luego el patrón se puede usar para una variedad de cosas útiles. Como:
- Evitar código repetitivo (
Maybe
) - Agregar características de lenguaje como estado mutable o excepciones para áreas delimitadas del programa.
- Aislar cosas repugnantes de cosas buenas (
IO
) - Idiomas integrados específicos de dominio (
Parser
) - Añadiendo GOTO al idioma.
Explicar "qué es una mónada" es un poco como decir "¿qué es un número?" Usamos números todo el tiempo. Pero imagina que conoces a alguien que no sabe nada sobre números. ¿ Cómo diablos explicarías qué son los números? ¿Y cómo comenzarías a describir por qué eso podría ser útil?
¿Qué es una mónada? La respuesta corta: es una forma específica de encadenar operaciones.
En esencia, estás escribiendo pasos de ejecución y vinculándolos con la "función de enlace". (En Haskell, se llama >>=
). Puede escribir las llamadas al operador de enlace usted mismo, o puede usar azúcar de sintaxis que hace que el compilador inserte esas llamadas a funciones por usted. Pero de cualquier manera, cada paso está separado por una llamada a esta función de enlace.
Entonces la función de vinculación es como un punto y coma; separa los pasos de un proceso. El trabajo de la función de enlace es tomar el resultado del paso anterior e introducirlo en el siguiente.
Eso no suena demasiado difícil, ¿verdad? Pero hay más de un tipo de mónada. ¿Por qué? ¿Cómo?
Bueno, la función de vinculación puede simplemente tomar el resultado de un paso y pasarlo al siguiente. Pero si eso es "todo" lo que hace la mónada... eso en realidad no es muy útil. Y es importante entender eso: cada mónada útil hace algo más además de ser simplemente una mónada. Cada mónada útil tiene un "poder especial", que la hace única.
(Una mónada que no hace nada especial se llama "mónada de identidad". Al igual que la función de identidad, esto suena como algo completamente inútil, pero resulta que no lo es... Pero esa es otra historia™.)
Básicamente, cada mónada tiene su propia implementación de la función de vinculación. Y puede escribir una función de enlace de modo que haga cosas entre los pasos de ejecución. Por ejemplo:
Si cada paso devuelve un indicador de éxito/fracaso, puede hacer que Bind ejecute el siguiente paso solo si el anterior tuvo éxito. De esta manera, un paso fallido aborta toda la secuencia "automáticamente", sin ninguna prueba condicional por su parte. (La mónada del fracaso .)
Ampliando esta idea, puede implementar "excepciones". (La mónada de error o la mónada de excepción ). Debido a que usted mismo las define en lugar de ser una característica del lenguaje, puede definir cómo funcionan. (Por ejemplo, tal vez desee ignorar las dos primeras excepciones y solo cancelar cuando se produzca una tercera excepción).
Puede hacer que cada paso devuelva múltiples resultados y hacer que la función de vinculación los recorra, introduciendo cada uno de ellos en el siguiente paso. De esta manera, no es necesario seguir escribiendo bucles por todos lados cuando se trata de múltiples resultados. La función de vinculación "automáticamente" hace todo eso por usted. (La mónada de la lista ).
Además de pasar un "resultado" de un paso a otro, también puedes hacer que la función de enlace pase datos adicionales . Estos datos ahora no aparecen en su código fuente, pero aún puede acceder a ellos desde cualquier lugar, sin tener que pasarlos manualmente a cada función. (La Mónada del Lector .)
Puede hacerlo para que se puedan reemplazar los "datos adicionales". Esto le permite simular actualizaciones destructivas , sin realizar realmente actualizaciones destructivas. (La Mónada del Estado y su prima la Mónada del Escritor .)
Debido a que solo estás simulando actualizaciones destructivas, puedes hacer cosas triviales que serían imposibles con actualizaciones destructivas reales . Por ejemplo, puedes deshacer la última actualización o volver a una versión anterior .
Puede crear una mónada donde se puedan pausar los cálculos , de modo que pueda pausar su programa, entrar y modificar los datos del estado interno y luego reanudarlo.
Puede implementar "continuaciones" como una mónada. ¡ Esto te permite romper la mente de las personas!
Todo esto y más es posible con las mónadas. Por supuesto, todo esto también es perfectamente posible sin mónadas. Es drásticamente más fácil usar mónadas.
En realidad, contrariamente a la comprensión común de las mónadas, no tienen nada que ver con el estado. Las mónadas son simplemente una forma de envolver cosas y proporcionar métodos para realizar operaciones en las cosas envueltas sin desenvolverlas.
Por ejemplo, puedes crear un tipo para envolver otro, en Haskell:
data Wrapped a = Wrap a
Para envolver cosas que definimos
return :: a -> Wrapped a
return x = Wrap x
Para realizar operaciones sin desenvolver, digamos que tiene una función f :: a -> b
, luego puede hacer esto para levantar esa función y actuar sobre los valores envueltos:
fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)
Eso es todo lo que hay que entender. Sin embargo, resulta que existe una función más general para hacer este levantamiento , que es bind
:
bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x
bind
Puede hacer un poco más que fmap
, pero no al revés. En realidad, fmap
sólo puede definirse en términos de bind
y return
. Entonces, al definir una mónada... das su tipo (aquí estaba Wrapped a
) y luego dices cómo funcionan sus operaciones return
y .bind
Lo bueno es que esto resulta ser un patrón tan general que aparece por todas partes, el estado encapsulado de forma pura es solo uno de ellos.
Para obtener un buen artículo sobre cómo se pueden usar las mónadas para introducir dependencias funcionales y así controlar el orden de evaluación, como se usa en la mónada IO de Haskell, consulte IO Inside .
En cuanto a la comprensión de las mónadas, no te preocupes demasiado. Lee sobre ellos lo que te parezca interesante y no te preocupes si no lo entiendes de inmediato. Entonces simplemente sumergirse en un lenguaje como Haskell es el camino a seguir. Las mónadas son una de esas cosas en las que la comprensión llega a tu cerebro con la práctica, y un día, de repente, te das cuenta de que las entiendes.
¡Pero podrías haber inventado las mónadas!
sigfpe dice:
Pero todos estos presentan a las mónadas como algo esotérico que necesita explicación. Pero lo que quiero argumentar es que no son esotéricos en absoluto. De hecho, ante diversos problemas de programación funcional, uno se habría visto conducido, inexorablemente, a ciertas soluciones, todas las cuales son ejemplos de mónadas. De hecho, espero que los inventes ahora si aún no lo has hecho. Entonces es un pequeño paso darse cuenta de que todas estas soluciones son, de hecho, la misma solución disfrazada. Y después de leer esto, es posible que estés en una mejor posición para comprender otros documentos sobre las mónadas porque reconocerás todo lo que veas como algo que ya has inventado.
Muchos de los problemas que las mónadas intentan solucionar están relacionados con la cuestión de los efectos secundarios. Entonces comenzaremos con ellos. (Tenga en cuenta que las mónadas le permiten hacer más que manejar efectos secundarios; en particular, muchos tipos de objetos contenedores pueden verse como mónadas. A algunas de las introducciones a las mónadas les resulta difícil conciliar estos dos usos diferentes de las mónadas y concentrarse en solo uno o el otro.)
En un lenguaje de programación imperativo como C++, las funciones no se comportan en nada como las funciones matemáticas. Por ejemplo, supongamos que tenemos una función de C++ que toma un único argumento de punto flotante y devuelve un resultado de punto flotante. Superficialmente podría parecer un poco como una función matemática que asigna reales a reales, pero una función de C++ puede hacer más que simplemente devolver un número que depende de sus argumentos. Puede leer y escribir los valores de variables globales, así como escribir resultados en la pantalla y recibir entradas del usuario. Sin embargo, en un lenguaje funcional puro, una función sólo puede leer lo que se le proporciona en sus argumentos y la única forma en que puede tener un efecto en el mundo es a través de los valores que devuelve.
Una mónada es un tipo de datos que tiene dos operaciones: >>=
(aka bind
) y return
(aka unit
). return
toma un valor arbitrario y crea una instancia de la mónada con él. >>=
toma una instancia de la mónada y asigna una función sobre ella. (Ya puedes ver que una mónada es un tipo de datos extraño, ya que en la mayoría de los lenguajes de programación no se puede escribir una función que tome un valor arbitrario y cree un tipo a partir de él. Las mónadas usan una especie de polimorfismo paramétrico ).
En notación Haskell, la interfaz de mónada está escrita
class Monad m where
return :: a -> m a
(>>=) :: forall a b . m a -> (a -> m b) -> m b
Se supone que estas operaciones obedecen ciertas "leyes", pero eso no es muy importante: las "leyes" simplemente codifican la forma en que deben comportarse las implementaciones sensatas de las operaciones (básicamente, eso >>=
y return
deben estar de acuerdo sobre cómo los valores se transforman en instancias de mónadas y eso >>=
es asociativo).
Las mónadas no se tratan solo de estado y E/S: abstraen un patrón común de cálculo que incluye trabajar con estado, E/S, excepciones y no determinismo. Probablemente las mónadas más sencillas de entender son las listas y los tipos de opciones:
instance Monad [ ] where
[] >>= k = []
(x:xs) >>= k = k x ++ (xs >>= k)
return x = [x]
instance Monad Maybe where
Just x >>= k = k x
Nothing >>= k = Nothing
return x = Just x
donde []
y :
son los constructores de la lista, ++
es el operador de concatenación y Just
y Nothing
son los Maybe
constructores. Ambas mónadas encapsulan patrones de cálculo comunes y útiles en sus respectivos tipos de datos (tenga en cuenta que ninguna tiene nada que ver con efectos secundarios o E/S).
Realmente tienes que jugar a escribir código Haskell no trivial para apreciar de qué se tratan las mónadas y por qué son útiles.