Para respetar a los lectores rápidos, primero comienzo con una definición precisa, continúo con una explicación más rápida y sencilla, y luego paso a ejemplos.
Aquí hay una definición concisa y precisa ligeramente reformulada:
Una mónada (en informática) es formalmente un mapa que:
envía cada tipo X
de algún lenguaje de programación dado a un nuevo tipo T(X)
(llamado "tipo de T
-computaciones con valores en X
");
equipado con una regla para componer dos funciones de la forma
f:X->T(Y)
y g:Y->T(Z)
para una función g∘f:X->T(Z)
;
de una manera asociativa en el sentido evidente y unital con respecto a una función de unidad dada llamada pure_X:X->T(X)
, que puede considerarse como tomar un valor a la computación pura que simplemente devuelve ese valor.
Entonces, en palabras simples, una mónada es una regla para pasar de cualquier tipo X
a otro tipoT(X)
, y una regla para pasar de dos funciones f:X->T(Y)
y g:Y->T(Z)
(que le gustaría componer pero no puede) a una nueva funciónh:X->T(Z)
. Que, sin embargo, no es la composición en sentido matemático estricto. Básicamente estamos "doblando" la composición de la función o redefiniendo cómo se componen las funciones.
Además, requerimos que la regla de composición de la mónada satisfaga los axiomas matemáticos "obvios":
- Asociatividad : componer
f
con g
y luego con h
(desde afuera) debería ser lo mismo que componer g
con h
y luego con f
(desde adentro).
- Propiedad unital : componer
f
con la función de identidad en cualquier lado debería rendir f
.
Nuevamente, en palabras simples, no podemos volvernos locos redefiniendo nuestra composición de funciones como nos gusta:
- Primero necesitamos la asociatividad para poder componer varias funciones en una fila
f(g(h(k(x)))
, por ejemplo , y no tener que preocuparnos de especificar el orden que compone los pares de funciones. Como la regla de la mónada solo prescribe cómo componer un par de funciones , sin ese axioma, necesitaríamos saber qué par se compone primero y así sucesivamente. (Tenga en cuenta que es diferente de la propiedad de conmutatividad que f
compuso con g
eran las mismas que g
compuestas con f
, lo cual no es obligatorio).
- Y segundo, necesitamos la propiedad unital, que es simplemente decir que las identidades se componen trivialmente de la forma en que las esperamos. Por lo tanto, podemos refactorizar las funciones de forma segura siempre que se puedan extraer esas identidades.
En resumen, una mónada es la regla de extensión de tipo y funciones de composición que satisfacen los dos axiomas: asociatividad y propiedad unital.
En términos prácticos, desea que la mónada sea implementada por el lenguaje, el compilador o el marco que se encargaría de componer las funciones por usted. Por lo tanto, puede concentrarse en escribir la lógica de su función en lugar de preocuparse de cómo se implementa su ejecución.
Eso es esencialmente eso, en pocas palabras.
Siendo matemático profesional, prefiero evitar llamar a h
la "composición" de f
y g
. Porque matemáticamente, no lo es. Llamarlo "composición" supone incorrectamente que h
es la verdadera composición matemática, que no lo es. Ni siquiera está determinado únicamente por f
y g
. En cambio, es el resultado de la nueva "regla de componer" las funciones de nuestra mónada. ¡Lo que puede ser totalmente diferente de la composición matemática real, incluso si existe esta última!
Para hacerlo menos seco, permítame intentar ilustrarlo con un ejemplo que estoy anotando con secciones pequeñas, para que pueda saltar directamente al punto.
Lanzamiento de excepciones como ejemplos de Monad
Supongamos que queremos componer dos funciones:
f: x -> 1 / x
g: y -> 2 * y
Pero f(0)
no está definido, por lo que e
se lanza una excepción . Entonces, ¿cómo puedes definir el valor compositivo g(f(0))
? ¡Lanza una excepción de nuevo, por supuesto! Quizás lo mismo e
. Quizás una nueva excepción actualizada e1
.
¿Qué sucede precisamente aquí? Primero, necesitamos nuevos valores de excepción (diferentes o iguales). Se les puede llamar nothing
o null
o lo que sea, pero la esencia sigue siendo la misma - que deben ser valores nuevos, por ejemplo, que no debe ser una number
en nuestro ejemplo aquí. Prefiero no llamarlos null
para evitar confusiones sobre cómo null
se puede implementar en un idioma específico. Igualmente, prefiero evitarlo nothing
porque a menudo está asociado con lo null
que, en principio, es lo que null
debería hacer, sin embargo, ese principio a menudo se dobla por cualquier razón práctica.
¿Qué es la excepción exactamente?
Este es un asunto trivial para cualquier programador experimentado, pero me gustaría dejar caer algunas palabras solo para extinguir cualquier gusano de confusión:
La excepción es un objeto que encapsula información sobre cómo se produjo el resultado no válido de la ejecución.
Esto puede variar desde descartar cualquier detalle y devolver un único valor global (como NaN
o null
) o generar una larga lista de registro o lo que sucedió exactamente, enviarlo a una base de datos y replicarlo en toda la capa de almacenamiento de datos distribuidos;)
La diferencia importante entre estos dos ejemplos extremos de excepción es que en el primer caso no hay efectos secundarios . En el segundo hay. Lo que nos lleva a la pregunta (de mil dólares):
¿Se permiten excepciones en funciones puras?
Respuesta más corta : Sí, pero solo cuando no conducen a efectos secundarios.
Respuesta larga Para ser puro, la salida de su función debe estar determinada únicamente por su entrada. Entonces modificamos nuestra función f
enviando 0
al nuevo valor abstracto e
que llamamos excepción. Nos aseguramos de que el valor e
no contenga información externa que no esté determinada únicamente por nuestra entrada, que es x
. Así que aquí hay un ejemplo de excepción sin efectos secundarios:
e = {
type: error,
message: 'I got error trying to divide 1 by 0'
}
Y aquí hay uno con efectos secundarios:
e = {
type: error,
message: 'Our committee to decide what is 1/0 is currently away'
}
En realidad, solo tiene efectos secundarios si ese mensaje puede cambiar en el futuro. Pero si se garantiza que nunca cambiará, ese valor se vuelve predecible de manera única, por lo que no hay efectos secundarios.
Para hacerlo aún más tonto. Una función que vuelve 42
siempre es claramente pura. Pero si alguien loco decide hacer 42
una variable cuyo valor podría cambiar, la misma función deja de ser pura en las nuevas condiciones.
Tenga en cuenta que estoy usando la notación literal del objeto por simplicidad para demostrar la esencia. Desafortunadamente, las cosas están desordenadas en lenguajes como JavaScript, donde error
no es un tipo que se comporta de la manera que queremos aquí con respecto a la composición de funciones, mientras que los tipos reales les gusta null
o NaN
no se comportan de esta manera, sino que pasan por algo artificial y no siempre intuitivo tipo de conversiones.
Extensión de tipo
Como queremos variar el mensaje dentro de nuestra excepción, realmente estamos declarando un nuevo tipo E
para todo el objeto de excepción y luego Eso es lo que maybe number
hace, aparte de su nombre confuso, que debe ser de tipo number
o del nuevo tipo de excepción E
, entonces es realmente la unión number | E
de number
y E
. En particular, depende de cómo queremos construir E
, que no se sugiere ni se refleja en el nombre maybe number
.
¿Qué es la composición funcional?
Se trata de las funciones matemáticas toma de la operación
f: X -> Y
y g: Y -> Z
, y la construcción de su composición como función de h: X -> Z
la satisfacción h(x) = g(f(x))
. El problema con esta definición ocurre cuando el resultado f(x)
no está permitido como argumento de g
.
En matemáticas, esas funciones no se pueden componer sin un trabajo adicional. La solución estrictamente matemática para nuestro ejemplo anterior de f
y g
es eliminar 0
del conjunto de definición de f
. Con ese nuevo conjunto de definición (nuevo tipo más restrictivo x
), f
se puede componer con g
.
Sin embargo, no es muy práctico en la programación restringir el conjunto de definiciones de f
ese tipo. En cambio, se pueden usar excepciones.
O como otro enfoque, los valores artificiales se crean como NaN
, undefined
, null
, Infinity
etc Por lo tanto se evalúa 1/0
a Infinity
y 1/-0
a -Infinity
. Y luego fuerce el nuevo valor nuevamente en su expresión en lugar de lanzar una excepción. Llevar a resultados que puede o no encontrar predecibles:
1/0 // => Infinity
parseInt(Infinity) // => NaN
NaN < 0 // => false
false + 1 // => 1
Y volvemos a los números regulares listos para seguir adelante;)
JavaScript nos permite seguir ejecutando expresiones numéricas a cualquier costo sin arrojar errores como en el ejemplo anterior. Eso significa que también permite componer funciones. De eso se trata exactamente la mónada: es una regla componer funciones que satisfagan los axiomas definidos al comienzo de esta respuesta.
Pero, ¿la regla de la función de composición, que surge de la implementación de JavaScript para tratar errores numéricos, es una mónada?
Para responder a esta pregunta, todo lo que necesita es verificar los axiomas (se deja como ejercicio como parte de la pregunta aquí;).
¿Se puede usar la excepción de lanzamiento para construir una mónada?
De hecho, una mónada más útil sería la regla que prescribe que si f
arroja una excepción para algunos x
, también lo hace su composición con cualquiera g
. Además, haga que la excepción sea E
globalmente única con un solo valor posible ( objeto terminal en la teoría de categorías). Ahora los dos axiomas son comprobables al instante y obtenemos una mónada muy útil. Y el resultado es lo que se conoce como quizás la mónada .