En programación funcional , una mónada es una estructura que combina fragmentos de programa ( funciones ) y envuelve sus valores de retorno en un tipo con computación adicional. Además de definir un tipo monádico envolvente , las mónadas definen dos operadores : uno para envolver un valor en el tipo de mónada y otro para componer funciones que generen valores del tipo de mónada (se conocen como funciones monádicas ). Los lenguajes de propósito general usan mónadas para reducir el código repetitivo necesario para operaciones comunes (como tratar con valores indefinidos o funciones falibles, o encapsular código de contabilidad). Los lenguajes funcionales usan mónadas para convertir secuencias complicadas de funciones en conductos sucintos que abstraen el flujo de control y los efectos secundarios . [1] [2]
Tanto el concepto de mónada como el término provienen originalmente de la teoría de categorías , donde una mónada se define como un funtor con estructura adicional. [a] La investigación que comenzó a fines de la década de 1980 y principios de la de 1990 estableció que las mónadas podían resolver problemas informáticos aparentemente dispares bajo un modelo funcional unificado. La teoría de categorías también proporciona algunos requisitos formales, conocidos como leyes de mónadas , que cualquier mónada debe satisfacer y que pueden usarse para verificar el código monádico. [3] [4]
Dado que las mónadas hacen explícita la semántica para un tipo de cálculo, también se pueden usar para implementar características convenientes del lenguaje. Algunos lenguajes, como Haskell , incluso ofrecen definiciones predefinidas en sus bibliotecas principales para la estructura general de la mónada y las instancias comunes. [1] [5]
"Para una mónada m
, un valor de tipo m a
representa tener acceso a un valor de tipo a
dentro del contexto de la mónada". —CA McCann [6]
Más exactamente, una mónada se puede utilizar cuando el acceso sin restricciones a un valor no es apropiado por razones específicas del escenario. En el caso de la mónada Maybe, es porque el valor puede no existir. En el caso de la mónada IO, es porque el valor puede no ser conocido todavía, como cuando la mónada representa la entrada del usuario que solo se proporcionará después de que se muestre un mensaje. En todos los casos, los escenarios en los que el acceso tiene sentido se capturan mediante la operación de enlace definida para la mónada; para la mónada Maybe, un valor se enlaza solo si existe, y para la mónada IO, un valor se enlaza solo después de que se hayan realizado las operaciones anteriores en la secuencia.
Se puede crear una mónada definiendo un constructor de tipo M y dos operaciones:
return :: a -> M a
(a menudo también llamado unidad ), que recibe un valor de tipo a
y lo envuelve en un valor monádico de tipo M a
, ybind :: (M a) -> (a -> M b) -> (M b)
(normalmente representado como >>=
), que recibe un valor monádico M a
y una función f
que acepta valores del tipo base a
. Bind desenvuelve a
, se aplica f
a él y puede procesar el resultado de f
como un valor monádico M b
.(Una construcción alternativa pero equivalente que utiliza la join
función en lugar del bind
operador se puede encontrar en la sección posterior § Derivación de funtores ).
Con estos elementos, el programador compone una secuencia de llamadas de función (una "tubería") con varios operadores de enlace encadenados en una expresión. Cada llamada de función transforma su valor de tipo simple de entrada y el operador de enlace maneja el valor monádico devuelto, que se introduce en el siguiente paso de la secuencia.
Normalmente, el operador de enlace >>=
puede contener código exclusivo de la mónada que realiza pasos de cálculo adicionales que no están disponibles en la función recibida como parámetro. Entre cada par de llamadas a funciones compuestas, el operador de enlace puede inyectar en el valor monádico m a
información adicional que no es accesible dentro de la función f
y pasarla a lo largo del proceso de ejecución. También puede ejercer un control más preciso del flujo de ejecución, por ejemplo, llamando a la función solo bajo ciertas condiciones o ejecutando las llamadas a la función en un orden particular.
Un ejemplo de mónada es el Maybe
tipo. Los resultados nulos indefinidos son un problema particular para el que muchos lenguajes procedimentales no proporcionan herramientas específicas, lo que requiere el uso del patrón de objeto nulo o comprobaciones para comprobar si hay valores no válidos en cada operación para manejar valores indefinidos. Esto provoca errores y dificulta la creación de software robusto que gestione los errores de forma elegante. El Maybe
tipo obliga al programador a lidiar con estos resultados potencialmente indefinidos definiendo explícitamente los dos estados de un resultado: Just ⌑result⌑
, o Nothing
. Por ejemplo, el programador podría estar construyendo un analizador, que debe devolver un resultado intermedio, o bien señalar una condición que el analizador ha detectado y que el programador también debe manejar. Con solo un poco de condimento funcional adicional, este Maybe
tipo se transforma en una mónada con todas las funciones. [b] : 12.3 páginas 148–151
En la mayoría de los lenguajes, la mónada Maybe también se conoce como un tipo de opción , que es simplemente un tipo que marca si contiene o no un valor. Normalmente se expresan como algún tipo de tipo enumerado . En este ejemplo de Rust lo llamaremos Maybe<T>
y las variantes de este tipo pueden ser un valor de tipo genérico T
o la variante vacía: Nothing
.
// La <T> representa una enumeración genérica de tipo "T" Maybe < T > { Just ( T ), Nothing , }
Maybe<T>
También puede entenderse como un tipo "envolvente", y aquí es donde entra en juego su conexión con las mónadas. En los lenguajes con alguna forma del Maybe
tipo, hay funciones que ayudan en su uso, como componer funciones monádicas entre sí y probar si a Maybe
contiene un valor.
En el siguiente ejemplo codificado, Maybe
se utiliza un tipo como resultado de funciones que pueden fallar, en este caso el tipo no devuelve nada si hay una división por cero .
fn divide ( x : Decimal , y : Decimal ) -> Maybe < Decimal > { if y == 0 { return Nothing } else { return Just ( x / y ) } } // divide(1.0, 4.0) -> devuelve Just(0.25) // divide(3.0, 0.0) -> devuelve Nothing
Una forma de comprobar si a Maybe
contiene o no un valor es utilizar if
declaraciones.
sea m_x = divide ( 3.14 , 0.0 ); // vea la función de división anterior // La declaración if extrae x de m_x if m_x es la variante Just de Maybe if let Just ( x ) = m_x { println! ( "respuesta: " , x ) } else { println! ( "la división falló, error de división por cero..." ) }
Otros idiomas pueden tener coincidencia de patrones
deje que resultado = dividir ( 3.0 , 2.0 ); coincidir resultado { Solo ( x ) => println! ( "Respuesta: " , x ), Nada => println! ( "la división falló; los conseguiremos la próxima vez." ), }
Las mónadas pueden componer funciones que devuelvan Maybe
, uniéndolas. Un ejemplo concreto podría ser una función que tome varios Maybe
parámetros y devuelva uno solo Maybe
cuyo valor sea Nothing
cuando cualquiera de los parámetros sea Nothing
, como en el siguiente ejemplo:
fn chainable_division ( maybe_x : Maybe < Decimal > , maybe_y : Maybe < Decimal > ) -> Maybe < Decimal > { match ( maybe_x , maybe_y ) { ( Just ( x ), Just ( y )) => { // Si ambas entradas son Just, comprueba la división por cero y divide en consecuencia if y == 0 { return Nothing } else { return Just ( x / y ) } }, _ => return Nothing // De lo contrario, devuelve Nothing } } chainable_division ( chainable_division ( Just ( 2.0 ), Just ( 0.0 )), Just ( 1.0 )); // dentro de chainable_division falla, fuera de chainable_division devuelve Nothing
En lugar de repetir Just
expresiones, podemos usar algo llamado operador de enlace (también conocido como "map", "flatmap" o "shove" [8] : 2205s ). Esta operación toma una mónada y una función que devuelve una mónada y ejecuta la función en el valor interno de la mónada pasada, devolviendo la mónada desde la función.
// Ejemplo de Rust que utiliza ".map". maybe_x se pasa a través de 2 funciones que devuelven Maybe<Decimal> y Maybe<String> respectivamente. // Al igual que con la composición de funciones normal, las entradas y salidas de las funciones que se alimentan entre sí deben coincidir con los tipos encapsulados. (es decir, la función add_one debe devolver un Maybe<Decimal> que luego se puede desempaquetar en un Decimal para la función decimal_to_string) let maybe_x : Maybe < Decimal > = Just ( 1.0 ) let maybe_result = maybe_x . map ( add_one ). map ( decimal_to_string )
En Haskell, hay un operador bind o ( >>=
) que permite esta composición monádica en una forma más elegante similar a la composición de funciones . [c] : 150–151
halve :: Int -> Maybe Int halve x | even x = Just ( x ` div ` 2 ) | odd x = Nothing -- Este código divide x por la mitad dos veces. Se evalúa como Nothing si x no es un múltiplo de 4 halve x >>= halve
Si >>=
está disponible, chainable_division
se puede expresar de forma mucho más sucinta con la ayuda de funciones anónimas (es decir, lambdas). Observe en la expresión siguiente cómo las dos lambdas anidadas operan cada una sobre el valor encapsulado en la Maybe
mónada pasada utilizando el operador de enlace. [d] : 93
división_encadenable ( mx , my ) = mx >>= ( λx -> my >>= ( λy -> Just ( x / y )) )
Lo que se ha demostrado hasta ahora es básicamente una mónada, pero para ser más conciso, lo que sigue es una lista estricta de cualidades necesarias para una mónada según se define en la siguiente sección.
Maybe
) [b] : 148–151 Just(x)
) [d] : 93 >>=
o .flatMap()
) [c] : 150–151 Estas son las tres cosas necesarias para formar una mónada. Otras mónadas pueden incorporar procesos lógicos diferentes y algunas pueden tener propiedades adicionales, pero todas ellas tendrán estos tres componentes similares. [1] [9]
La definición más común de mónada en programación funcional, utilizada en el ejemplo anterior, se basa en realidad en una tripleta de Kleisli ⟨T, η, μ⟩ en lugar de la definición estándar de la teoría de categorías. Sin embargo, las dos construcciones resultan ser matemáticamente equivalentes, por lo que cualquiera de las dos definiciones dará como resultado una mónada válida. Dados los tipos básicos T y U bien definidos , una mónada consta de tres partes:
unit : T → M T
[F]>>=
o un método llamado flatMap , que desenvuelve una variable monádica y luego la inserta en una función/expresión monádica, lo que da como resultado un nuevo valor monádico:(>>=) : (M T, T → M U) → M U
[g] así que si mx : M T
y f : T → M U
, entonces (mx >>= f) : M U
Sin embargo, para poder ser consideradas plenamente como mónada, estas tres partes también deben respetar algunas leyes:
unit(x) >>= f
↔ f(x)
ma >>= unit
↔ ma
ma >>= λx → (f(x) >>= g)
↔ (ma >>= f) >>= g
[1]Algebraicamente, esto significa que cualquier mónada da lugar a una categoría (llamada categoría de Kleisli ) y a un monoide en la categoría de funtores (desde valores hasta cálculos), con composición monádica como operador binario en el monoide [8] : 2450s y unidad como identidad en el monoide.
El valor del patrón mónada va más allá de simplemente condensar el código y proporcionar un vínculo al razonamiento matemático. Cualquiera sea el lenguaje o paradigma de programación predeterminado que utilice un desarrollador, seguir el patrón mónada aporta muchos de los beneficios de la programación puramente funcional . Al materializar un tipo específico de cálculo, una mónada no solo encapsula los tediosos detalles de ese patrón computacional, sino que lo hace de manera declarativa , mejorando la claridad del código. Como los valores monádicos representan explícitamente no solo valores calculados, sino también efectos calculados , una expresión monádica puede reemplazarse con su valor en posiciones referencialmente transparentes , de manera muy similar a las expresiones puras, lo que permite muchas técnicas y optimizaciones basadas en la reescritura . [4]
Por lo general, los programadores usan bind para encadenar funciones monádicas en una secuencia, lo que ha llevado a algunos a describir las mónadas como "punto y coma programables", una referencia a cómo muchos lenguajes imperativos usan punto y coma para separar declaraciones . [1] [5] Sin embargo, las mónadas en realidad no ordenan los cálculos; incluso en lenguajes que las usan como características centrales, la composición de funciones más simple puede organizar los pasos dentro de un programa. La utilidad general de una mónada radica más bien en simplificar la estructura de un programa y mejorar la separación de preocupaciones a través de la abstracción. [4] [11]
La estructura de mónada también puede verse como una variación exclusivamente matemática y de tiempo de compilación del patrón decorador . Algunas mónadas pueden pasar datos adicionales que son inaccesibles para las funciones, y algunas incluso ejercen un control más preciso sobre la ejecución, por ejemplo, solo invocando una función bajo ciertas condiciones. Debido a que permiten a los programadores de aplicaciones implementar la lógica del dominio mientras descargan código repetitivo en módulos desarrollados previamente, las mónadas incluso pueden considerarse una herramienta para la programación orientada a aspectos . [12]
Otro uso notable de las mónadas es el aislamiento de efectos secundarios, como entrada/salida o estado mutable , en código que de otro modo sería puramente funcional. Incluso los lenguajes puramente funcionales pueden implementar estos cálculos "impuros" sin mónadas, a través de una intrincada combinación de composición de funciones y estilo de paso de continuación (CPS) en particular. [2] Sin embargo, con las mónadas, gran parte de este andamiaje se puede abstraer, esencialmente tomando cada patrón recurrente en el código CPS y agrupándolo en una mónada distinta. [4]
Si un lenguaje no admite mónadas por defecto, aún es posible implementar el patrón, a menudo sin mucha dificultad. Cuando se traduce de la teoría de categorías a términos de programación, la estructura de mónada es un concepto genérico y se puede definir directamente en cualquier lenguaje que admita una característica equivalente para el polimorfismo acotado . La capacidad de un concepto de permanecer agnóstico sobre los detalles operativos mientras trabaja en tipos subyacentes es poderosa, pero las características únicas y el comportamiento estricto de las mónadas las distinguen de otros conceptos. [13]
Los debates sobre mónadas específicas se centran normalmente en la solución de un problema de implementación específico, ya que una mónada determinada representa una forma computacional específica. Sin embargo, en algunas situaciones, una aplicación puede incluso alcanzar sus objetivos de alto nivel utilizando mónadas adecuadas dentro de su lógica central.
Estas son solo algunas aplicaciones que tienen mónadas en el corazón de sus diseños:
El término "mónada" en programación data de los lenguajes de programación APL y J , que tienden a ser puramente funcionales. Sin embargo, en esos lenguajes, "mónada" es solo una forma abreviada de referirse a una función que toma un parámetro (una función con dos parámetros es una "díada", y así sucesivamente). [19]
El matemático Roger Godement fue el primero en formular el concepto de mónada (llamándolo una "construcción estándar") a fines de la década de 1950, aunque el término "mónada" que llegó a dominar fue popularizado por el teórico de categorías Saunders Mac Lane . [ cita requerida ] Sin embargo, la forma definida anteriormente utilizando bind fue descrita originalmente en 1965 por el matemático Heinrich Kleisli para demostrar que cualquier mónada podía caracterizarse como una adjunción entre dos funtores (covariantes). [20]
A partir de la década de 1980, una noción vaga del patrón mónada comenzó a surgir en la comunidad de la ciencia informática. Según el investigador de lenguajes de programación Philip Wadler , el científico informático John C. Reynolds anticipó varias facetas de este patrón en la década de 1970 y principios de la de 1980, cuando analizó el valor del estilo de paso de continuación , de la teoría de categorías como una rica fuente de semántica formal y de la distinción de tipos entre valores y cálculos. [4] El lenguaje de investigación Opal , que se diseñó activamente hasta 1990, también basó efectivamente la E/S en un tipo monádico, pero la conexión no se realizó en ese momento. [21]
El científico informático Eugenio Moggi fue el primero en vincular explícitamente la mónada de la teoría de categorías con la programación funcional, en un artículo de conferencia en 1989, [22] seguido por una presentación más refinada en una revista en 1991. En trabajos anteriores, varios científicos informáticos habían avanzado en el uso de la teoría de categorías para proporcionar semántica para el cálculo lambda . La idea clave de Moggi fue que un programa del mundo real no es solo una función de valores a otros valores, sino más bien una transformación que forma cálculos sobre esos valores. Cuando se formaliza en términos de teoría de categorías, esto lleva a la conclusión de que las mónadas son la estructura para representar estos cálculos. [3]
Varios otros popularizaron y desarrollaron esta idea, incluidos Philip Wadler y Simon Peyton Jones , quienes estuvieron involucrados en la especificación de Haskell. En particular, Haskell utilizó un modelo problemático de "flujo diferido" hasta la v1.2 para reconciliar la E/S con la evaluación diferida , hasta que cambió a una interfaz monádica más flexible. [23] La comunidad Haskell continuaría aplicando mónadas a muchos problemas en programación funcional y, en la década de 2010, los investigadores que trabajaban con Haskell finalmente reconocieron que las mónadas son funtores aplicativos ; [24] [i] y que tanto las mónadas como las flechas son monoides . [26]
Al principio, la programación con mónadas se limitaba en gran medida a Haskell y sus derivados, pero a medida que la programación funcional influyó en otros paradigmas, muchos lenguajes incorporaron un patrón de mónada (en espíritu, si no en nombre). Ahora existen formulaciones en Scheme , Perl , Python , Racket , Clojure , Scala , F# y también se han considerado para un nuevo estándar de ML . [ cita requerida ]
Una ventaja del patrón de mónada es que aporta precisión matemática a la composición de los cálculos. No solo se pueden utilizar las leyes de mónada para comprobar la validez de una instancia, sino que también se pueden utilizar características de estructuras relacionadas (como los funtores) mediante la subtipificación .
Volviendo al Maybe
ejemplo, se declaró que sus componentes formaban una mónada, pero no se dio ninguna prueba de que satisficiera las leyes de la mónada.
Esto se puede corregir introduciendo los detalles de Maybe
en un lado de las leyes generales y luego construyendo algebraicamente una cadena de igualdades para llegar al otro lado:
Ley 1: eta(a) >>= f(x) ⇔ (Solo a) >>= f(x) ⇔ f(a)
Ley 2: ma >>= eta(x) ⇔ ma Si ma es (Solo a) entonces eta(a) ⇔ Solo una De lo contrario o Nada ⇔ Nada terminar si
Ley 3: ( ma >>= f(x) ) >>= g(y) ⇔ ma >>= ( f(x) >>= g(y) ) si (ma >>= f(x)) es (Solo b) entonces si ma es (Solo a) entonces g(ma >>= f(x)) (f(x) >>= g(y)) a otra cosa otra cosa Nada, nada fin si fin si ⇔ si ma es (Solo a) y f(a) es (Solo b) entonces (g ∘ f) a De lo contrario, si ma es (solo a) y f(a) no es nada, entonces Nada demás Nada terminar si
Aunque es menos frecuente en informática, se puede utilizar directamente la teoría de categorías, que define una mónada como un funtor con dos transformaciones naturales adicionales . [j] Entonces, para comenzar, una estructura requiere una función de orden superior (o "funcional") llamada mapa para calificar como un funtor:
map : (a → b) → (ma → mb)
Sin embargo, esto no siempre es un problema importante, especialmente cuando una mónada se deriva de un funtor preexistente, en cuyo caso la mónada hereda map automáticamente. (Por razones históricas, en Haskell map
se le llama así fmap
).
La primera transformación de una mónada es en realidad la misma unidad del triple de Kleisli, pero siguiendo de cerca la jerarquía de estructuras, resulta que unidad caracteriza a un funtor aplicativo , una estructura intermedia entre una mónada y un funtor básico. En el contexto aplicativo, a veces se hace referencia a unidad como pura , pero sigue siendo la misma función. Lo que difiere en esta construcción es la ley que debe satisfacer la unidad ; como no se define el vínculo , la restricción se da en términos de mapa :
(unit ∘ φ) x ↔ ((map φ) ∘ unit) x ↔ x
[27]El salto final del funtor aplicativo a la mónada llega con la segunda transformación, la función de unión (en teoría de categorías esta es una transformación natural usualmente llamada μ ), que "aplana" las aplicaciones anidadas de la mónada:
join(mma) : M (M T) → M T
Como función característica, join también debe satisfacer tres variaciones de las leyes de la mónada:
(join ∘ (map join)) mmma ↔ (join ∘ join) mmma ↔ ma
(join ∘ (map unit)) ma ↔ (join ∘ unit) ma ↔ ma
(join ∘ (map map φ)) mma ↔ ((map φ) ∘ join) mma ↔ mb
Independientemente de si un desarrollador define una mónada directa o un triple de Kleisli, la estructura subyacente será la misma y las formas se pueden derivar entre sí fácilmente:
(map φ) ma ↔ ma >>= (unit ∘ φ)
join(mma) ↔ mma >>= id
ma >>= f ↔ (join ∘ (map f)) ma
[28]La mónada List demuestra naturalmente cómo puede resultar útil derivar una mónada a partir de un funtor más simple. En muchos lenguajes, una estructura de lista viene predefinida junto con algunas características básicas, por lo que se supone que ya se han proporcionado aquí un List
constructor de tipos y un operador de anexión (representado con ++
la notación infija for).
Incrustar un valor simple en una lista también es trivial en la mayoría de los lenguajes:
unidad(x) = [x]
A partir de aquí, aplicar una función de forma iterativa con una comprensión de lista puede parecer una opción fácil para bind y convertir listas en una mónada completa. La dificultad con este enfoque es que bind espera funciones monádicas, que en este caso generarán listas por sí mismas; a medida que se apliquen más funciones, se acumularán capas de listas anidadas, lo que requerirá más que una comprensión básica.
Sin embargo, un procedimiento para aplicar cualquier función simple sobre toda la lista, en otras palabras map , es sencillo:
(mapa φ) listax = [ φ(x1), φ(x2), ..., φ(xn) ]
Ahora bien, estos dos procedimientos ya promueven List
un funtor aplicativo. Para calificar plenamente como mónada, solo se necesita una noción correcta de unión para aplanar la estructura repetida, pero para las listas, eso solo significa desenvolver una lista externa para agregar las internas que contienen valores:
unir(xlistlist) = unir([xlist1, xlist2, ..., xlistn]) = listax1 ++ listax2 ++ ... ++ listaxn
La mónada resultante no es sólo una lista, sino una que se redimensiona y condensa automáticamente a medida que se aplican funciones. bind ahora también se puede derivar con sólo una fórmula y luego usarse para introducir List
valores a través de una tubería de funciones monádicas:
(xlist >>= f) = unir ∘ (mapa f) xlist
Una aplicación para esta lista monádica es representar el cálculo no determinista . List
Puede contener resultados para todas las rutas de ejecución en un algoritmo, luego condensarse en cada paso para "olvidar" qué rutas llevaron a qué resultados (una distinción a veces importante de los algoritmos deterministas y exhaustivos). [ cita requerida ]
Otro beneficio es que las verificaciones se pueden incrustar en la mónada; las rutas específicas se pueden podar de forma transparente en su primer punto de falla, sin necesidad de reescribir funciones en la tubería. [28]
Una segunda situación en la que List
brilla es la composición de funciones multivaluadas . Por ejemplo, la raíz compleja n -ésima de un número debería producir n números complejos distintos, pero si luego se toma otra raíz m -ésima de esos resultados, los valores m•n finales deberían ser idénticos a la salida de la raíz m•n -ésima. automatiza completamente este problema, condensando los resultados de cada paso en una lista plana y matemáticamente correcta. [29]List
Las mónadas ofrecen oportunidades para técnicas interesantes que van más allá de la simple organización de la lógica del programa. Las mónadas pueden sentar las bases para características sintácticas útiles, mientras que su naturaleza matemática y de alto nivel permite una abstracción significativa.
Aunque usar bind abiertamente suele tener sentido, muchos programadores prefieren una sintaxis que imite las declaraciones imperativas (llamada do-notation en Haskell, perform-notation en OCaml , expresiones computacionales en F# , [30] y para comprensión en Scala ). Esto es solo azúcar sintáctico que disfraza una tubería monádica como un bloque de código ; el compilador luego traducirá silenciosamente estas expresiones en código funcional subyacente.
La traducción de la add
función de a Maybe
Haskell puede mostrar esta característica en acción. Una versión no monádica de add
en Haskell se ve así:
agregar mx my = caso mx de Nada -> Nada Solo x -> caso my de Nada -> Nada Solo y -> Solo ( x + y )
En Haskell monádico, return
es el nombre estándar para unidad , además las expresiones lambda deben manejarse explícitamente, pero incluso con estos tecnicismos, la Maybe
mónada permite una definición más clara:
añadir mx mi = mx >>= ( \ x -> mi >>= ( \ y -> return ( x + y )))
Sin embargo, con la notación do, esto se puede resumir aún más en una secuencia muy intuitiva:
suma mx my = hacer x <- mx y <- my return ( x + y )
Un segundo ejemplo muestra cómo Maybe
se puede utilizar en un lenguaje completamente diferente: F#. Con expresiones de cálculo, una función de "división segura" que retorna None
para un operando indefinido o una división por cero se puede escribir como:
deje que readNum () = deje que s = Console.ReadLine ( ) deje que succ , v = Int32.TryParse ( s ) si ( succ ) entonces Algún ( v ) de lo contrario Ninguno deje que secure_div = tal vez { deje que ! x = readNum () deje que ! y = readNum () si ( y = 0 ) entonces Ninguno de lo contrario devuelva ( x / y ) }
En el momento de la compilación, el compilador "desendulzará" internamente esta función en una cadena más densa de llamadas de enlace :
tal vez . Delay ( fun () -> tal vez . Bind ( readNum () , fun x -> tal vez . Bind ( readNum () , fun y -> if ( y = 0 ) then None else tal vez . Return ( x / y ))))
Como último ejemplo, incluso las leyes generales de la mónada pueden expresarse en notación do:
hacer { x <- devolver v ; f x } == hacer { f v } hacer { x <- m ; devolver x } == hacer { m } hacer { y <- hacer { x < - m ; f x }; g y } == hacer { x <- m ; y <- f x ; g y }
Cada mónada necesita una implementación específica que cumpla con las leyes de mónadas, pero otros aspectos como la relación con otras estructuras o modismos estándar dentro de un lenguaje son compartidos por todas las mónadas. Como resultado, un lenguaje o biblioteca puede proporcionar una Monad
interfaz general con prototipos de funciones , relaciones de subtipos y otros hechos generales. Además de proporcionar una ventaja para el desarrollo y garantizar que una nueva mónada herede características de un supertipo (como los funtores), verificar el diseño de una mónada con respecto a la interfaz agrega otra capa de control de calidad. [ cita requerida ]
El código monádico puede simplificarse aún más mediante el uso juicioso de operadores. La función map puede ser especialmente útil, ya que funciona en más que funciones monádicas ad hoc; siempre que una función monádica funcione de manera análoga a un operador predefinido, map puede usarse para " elevar " instantáneamente el operador más simple a uno monádico. [k]
Con esta técnica, la definición del add
ejemplo Maybe
podría resumirse en:
agregar(mx,my) = mapa (+)
El proceso podría llevarse incluso un paso más allá definiendo add
no solo for Maybe
, sino toda la Monad
interfaz. Al hacer esto, cualquier nueva mónada que coincida con la estructura interface e implemente su propio mapa heredará inmediatamente una versión elevada de add
too. El único cambio necesario en la función es generalizar la firma de tipo:
añadir: (Número de mónada, Número de mónada) → Número de mónada [31]
Otro operador monádico que también es útil para el análisis es la composición monádica (representada >=>
aquí como infijo), que permite encadenar funciones monádicas en un estilo más matemático:
(f >=> g)(x) = f(x) >>= g
Con este operador, las leyes de la mónada se pueden escribir solo en términos de funciones, resaltando la correspondencia con la asociatividad y la existencia de una identidad:
(unidad >=> g) ↔ g(f >=> unidad) ↔ f(f >=> g) >=> h ↔ f >=> (g >=> h) [1]
A su vez, lo anterior muestra el significado del bloque "do" en Haskell:
hacer _p <- f(x) _q <- g(_p) h(_q) ↔ ( f >=> g >=> h )(x)
La mónada más simple es la mónada Identidad , que simplemente anota valores y funciones simples para satisfacer las leyes de la mónada:
nuevo tipo Id T = Tunidad(x) = x(x >>= f) = f(x)
Identity
Sin embargo, en realidad tiene usos válidos, como proporcionar un caso base para transformadores de mónadas recursivos . También se puede utilizar para realizar asignaciones de variables básicas dentro de un bloque de estilo imperativo. [l] [ cita requerida ]
Cualquier colección con un append adecuado ya es un monoide libre, pero resulta que List
no es la única colección que también tiene una unión bien definida y califica como una mónada. Uno puede incluso mutar List
en estas otras colecciones monádicas simplemente imponiendo propiedades especiales en append : [m] [n]
Como ya se mencionó, el código puro no debería tener efectos secundarios no administrados, pero eso no impide que un programa describa y administre explícitamente los efectos. Esta idea es central para la mónada IO de Haskell , donde un objeto de tipo IO a
puede verse como una descripción de una acción a realizar en el mundo, proporcionando opcionalmente información sobre el mundo de tipo a
. Una acción que no proporciona información sobre el mundo tiene el tipo IO ()
, "proporcionando" el valor ficticio ()
. Cuando un programador vincula un IO
valor a una función, la función calcula la siguiente acción a realizar en función de la información sobre el mundo proporcionada por la acción anterior (entrada de usuarios, archivos, etc.). [23] Lo más significativo es que, debido a que el valor de la mónada IO solo se puede vincular a una función que calcula otra mónada IO, la función de vinculación impone una disciplina de una secuencia de acciones donde el resultado de una acción solo se puede proporcionar a una función que calculará la siguiente acción a realizar. Esto significa que las acciones que no necesitan ser realizadas nunca se realizan, y las acciones que sí necesitan ser realizadas tienen una secuencia bien definida, lo que resuelve el problema de que las acciones (IO) no sean referencialmente transparentes .
Por ejemplo, Haskell tiene varias funciones para actuar sobre el sistema de archivos más amplio , incluida una que verifica si existe un archivo y otra que lo elimina. Sus dos firmas de tipo son:
doesFileExist :: RutaArchivo -> IO Bool removeFile :: RutaArchivo -> IO ()
La primera se interesa por si un archivo determinado existe realmente y, como resultado, genera un valor booleano dentro de la IO
mónada. La segunda función, por otro lado, solo se ocupa de actuar sobre el sistema de archivos, por lo que el IO
contenedor que genera está vacío.
IO
Sin embargo, no se limita solo a la E/S de archivos; también permite la E/S de usuario y, junto con la sintaxis imperativa, puede imitar un programa típico de "¡Hola, mundo!" :
main :: IO () main = do putStrLn "Hola, mundo!" putStrLn "¿Cuál es tu nombre, usuario?" name <- getLine putStrLn ( "Un placer conocerte, " ++ name ++ "!" )
Sin azúcar, esto se traduce en la siguiente canalización monádica ( >>
en Haskell es solo una variante de bind para cuando solo importan los efectos monádicos y el resultado subyacente se puede descartar):
main :: IO () main = putStrLn "¡Hola, mundo!" >> putStrLn "¿Cuál es tu nombre, usuario?" >> getLine >>= ( \ name -> putStrLn ( "Encantado de conocerte, " ++ name ++ "!" ))
Otra situación habitual es la de mantener un archivo de registro o informar de algún otro modo sobre el progreso de un programa. A veces, un programador puede querer registrar datos técnicos aún más específicos para su posterior elaboración de perfiles o depuración . La mónada Writer puede encargarse de estas tareas generando una salida auxiliar que se acumula paso a paso.
Para demostrar que el patrón de mónada no está restringido a lenguajes principalmente funcionales, este ejemplo implementa una Writer
mónada en JavaScript . Primero, una matriz (con colas anidadas) permite construir el Writer
tipo como una lista enlazada . El valor de salida subyacente estará en la posición 0 de la matriz, y la posición 1 contendrá implícitamente una cadena de notas auxiliares:
const escritor = valor => [ valor , []];
Definir unidad también es muy sencillo:
const unidad = valor => [ valor , []];
Solo se necesita una unidad para definir funciones simples que generen Writer
objetos con notas de depuración:
const al cuadrado = x => [ x * x , [ ` ${ x } fue elevado al cuadrado.` ]]; const reducido a la mitad = x => [ x / 2 , [ ` ${ x } fue reducido a la mitad.` ]];
Una verdadera mónada aún requiere bind , pero para Writer
, esto equivale simplemente a concatenar la salida de una función a la lista enlazada de la mónada:
const bind = ( escritor , transformar ) => { const [ valor , registro ] = escritor ; const [ resultado , actualizaciones ] = transformar ( valor ); return [ resultado , registro . concat ( actualizaciones )]; };
Las funciones de muestra ahora se pueden encadenar entre sí mediante bind , pero definir una versión de composición monádica (llamada pipelog
aquí) permite aplicar estas funciones de manera aún más sucinta:
const pipelog = ( escritor , ... transforma ) => transforma . reduce ( bind , escritor );
El resultado final es una clara separación de preocupaciones entre la ejecución de los cálculos y su registro para una auditoría posterior:
pipelog ( unidad ( 4 ), al cuadrado , reducido a la mitad ); // Objeto de escritor resultante = [8, ['4 fue elevado al cuadrado.', '16 fue reducido a la mitad.']]
Una mónada de entorno (también llamada mónada de lectura y mónada de función ) permite que un cálculo dependa de valores de un entorno compartido. El constructor de tipo de mónada asigna un tipo T a funciones de tipo E → T , donde E es el tipo del entorno compartido. Las funciones de mónada son:
Las siguientes operaciones monádicas son útiles:
La operación de solicitud se utiliza para recuperar el contexto actual, mientras que la operación local ejecuta un cálculo en un subcontexto modificado. Al igual que en una mónada de estado, los cálculos en la mónada de entorno se pueden invocar simplemente proporcionando un valor de entorno y aplicándolo a una instancia de la mónada.
Formalmente, un valor en una mónada de entorno es equivalente a una función con un argumento anónimo adicional; return y bind son equivalentes a los combinadores K y S , respectivamente, en el cálculo del combinador SKI .
Una mónada de estado permite a un programador adjuntar información de estado de cualquier tipo a un cálculo. Dado cualquier tipo de valor, el tipo correspondiente en la mónada de estado es una función que acepta un estado y luego genera un nuevo estado (de tipo s
) junto con un valor de retorno (de tipo t
). Esto es similar a una mónada de entorno, excepto que también devuelve un nuevo estado y, por lo tanto, permite modelar un entorno mutable .
tipo Estado s t = s -> ( t , s )
Tenga en cuenta que esta mónada toma un parámetro de tipo, el tipo de información de estado. Las operaciones de la mónada se definen de la siguiente manera:
-- "return" produce el valor dado sin cambiar el estado. return x = \ s -> ( x , s ) -- "bind" modifica m para que aplique f a su resultado. m >>= f = \ r -> let ( x , s ) = m r in ( f x ) s
Las operaciones estatales útiles incluyen:
get = \ s -> ( s , s ) -- Examina el estado en este punto del cálculo. put s = \ _ -> ( () , s ) -- Reemplaza el estado. modify f = \ s -> ( () , f s ) -- Actualiza el estado
Otra operación aplica una mónada de estado a un estado inicial dado:
estadoDeEjecución :: Estado s a -> s -> ( a , s ) estadoDeEjecución t s = t s
Los bloques do en una mónada de estado son secuencias de operaciones que pueden examinar y actualizar los datos de estado.
De manera informal, una mónada de estado de tipo de estado S asigna el tipo de valores de retorno T a funciones de tipo , donde S es el estado subyacente. Las funciones de retorno y de enlace son:
Desde el punto de vista de la teoría de categorías, una mónada de estado se deriva de la unión entre el funtor producto y el funtor exponencial, que existe en cualquier categoría cartesiana cerrada por definición.
Una mónada de continuación [o] con tipo de retorno R asigna el tipo T a funciones de tipo . Se utiliza para modelar el estilo de paso de continuación . Las funciones de retorno y enlace son las siguientes:
La función de llamada con continuación actual se define de la siguiente manera:
El siguiente código es pseudocódigo.Supongamos que tenemos dos funciones foo
y bar
, con tipos
foo : int -> int bar : int -> int
Es decir, ambas funciones toman un entero y devuelven otro entero. Luego podemos aplicar las funciones en sucesión de la siguiente manera:
foo ( barra x )
Donde el resultado es el resultado de foo
aplicado al resultado de bar
aplicado a x
.
Pero supongamos que estamos depurando nuestro programa y queremos agregar mensajes de registro a foo
y bar
. Por lo tanto, cambiamos los tipos de la siguiente manera:
foo : int -> int * cadena bar : int -> int * cadena
Para que ambas funciones devuelvan una tupla, con el resultado de la aplicación como entero, y un mensaje de registro con información sobre la función aplicada y todas las funciones aplicadas previamente como cadena.
Lamentablemente, esto significa que ya no podemos componer foo
y bar
, ya que su tipo de entrada int
no es compatible con su tipo de salida int * string
. Y aunque podemos volver a ganar capacidad de composición modificando los tipos de cada función para que sean int * string -> int * string
, esto requeriría que agreguemos código repetitivo a cada función para extraer el entero de la tupla, lo que se volvería tedioso a medida que aumenta la cantidad de dichas funciones.
En lugar de eso, definamos una función auxiliar para abstraer este código estándar para nosotros:
enlazar : int * cadena -> ( int -> int * cadena ) -> int * cadena
bind
toma una tupla de números enteros y cadenas, y luego toma una función (como foo
) que asigna un número entero a una tupla de números enteros y cadenas. Su salida es una tupla de números enteros y cadenas, que es el resultado de aplicar la función de entrada al número entero dentro de la tupla de números enteros y cadenas de entrada. De esta manera, solo necesitamos escribir código repetitivo para extraer el número entero de la tupla una vez, en bind
.
Ahora hemos recuperado cierta componibilidad. Por ejemplo:
enlazar ( enlazar ( x , s ) bar ) foo
Donde (x,s)
es una tupla de números enteros y cadenas. [p]
Para que los beneficios sean aún más claros, definamos un operador infijo como un alias para bind
:
( >>= ) : int * cadena -> ( int -> int * cadena ) -> int * cadena
Así que t >>= f
es lo mismo que bind t f
.
Entonces el ejemplo anterior se convierte en:
(( x , s ) >>= barra ) >>= foo
Por último, definimos una nueva función para evitar escribir (x, "")
cada vez que deseamos crear un mensaje de registro vacío, donde ""
es la cadena vacía.
retorno : int -> int * cadena
Que se envuelve x
en la tupla descrita anteriormente.
El resultado es una canalización para registrar mensajes:
(( devuelve x ) >>= barra ) >>= foo
Esto nos permite registrar más fácilmente los efectos de bar
y foo
en x
.
int * string
denota un valor monádico pseudocodificado . [p] bind
y return
son análogas a las funciones correspondientes del mismo nombre. De hecho, int * string
, bind
, y return
forman una mónada.
Una mónada aditiva es una mónada dotada de un operador binario asociativo cerrado adicional mplus y un elemento identidad bajo mplus , llamado mzero . La Maybe
mónada puede considerarse aditiva, con Nothing
mzero y una variación del operador OR como mplus . List
también es una mónada aditiva, con la lista vacía []
actuando como mzero y el operador de concatenación ++
como mplus .
Intuitivamente, mzero representa un contenedor monádico sin valor de un tipo subyacente, pero también se considera un "cero" (en lugar de un "uno") ya que actúa como un absorbedor para bind , devolviendo mzero siempre que se enlaza a una función monádica. Esta propiedad es bilateral y bind también devolverá mzero cuando cualquier valor esté enlazado a una función cero monádica .
En términos de teoría de categorías, una mónada aditiva califica una vez como un monoide sobre funciones monádicas con vínculo (como lo hacen todas las mónadas), y nuevamente sobre valores monádicos a través de mplus . [32] [q]
A veces, el esquema general de una mónada puede ser útil, pero no existe ningún patrón simple que recomiende una u otra mónada. Aquí es donde entra en juego una mónada libre ; como objeto libre en la categoría de mónadas, puede representar una estructura monádica sin ninguna restricción específica más allá de las propias leyes de la mónada. Así como un monoide libre concatena elementos sin evaluación, una mónada libre permite encadenar cálculos con marcadores para satisfacer el sistema de tipos, pero por lo demás no impone ninguna semántica más profunda.
Por ejemplo, al trabajar completamente a través de los marcadores Just
y Nothing
, la Maybe
mónada es de hecho una mónada libre. La List
mónada, por otro lado, no es una mónada libre ya que incorpora hechos adicionales y específicos sobre listas (como append ) a su definición. Un último ejemplo es una mónada libre abstracta:
datos Libre f a = Puro a | Libre ( f ( Libre f a )) unidad :: a -> Libre f a unidad x = Pura x bind :: Functor f => Free f a -> ( a -> Free f b ) -> Free f b bind ( Pure x ) f = f x bind ( Free x ) f = Free ( fmap ( \ y -> bind y f ) x )
Sin embargo, las mónadas libres no están restringidas a una lista enlazada como en este ejemplo, y pueden construirse alrededor de otras estructuras como árboles .
El uso intencional de mónadas libres puede parecer poco práctico al principio, pero su naturaleza formal es particularmente adecuada para problemas sintácticos. Una mónada libre se puede utilizar para rastrear la sintaxis y el tipo, dejando la semántica para más adelante, y como resultado, se ha encontrado uso en analizadores sintácticos e intérpretes . [33] Otros también las han aplicado a problemas operativos más dinámicos, como proporcionar iteradores dentro de un lenguaje. [34]
Además de generar mónadas con propiedades adicionales, para cualquier mónada dada, también se puede definir una comónada . Conceptualmente, si las mónadas representan cálculos construidos a partir de valores subyacentes, entonces las comónadas pueden verse como reducciones de vuelta a los valores. El código monádico, en cierto sentido, no se puede "desempaquetar" por completo; una vez que un valor se envuelve dentro de una mónada, permanece en cuarentena allí junto con cualquier efecto secundario (algo bueno en la programación puramente funcional). Sin embargo, a veces, un problema tiene más que ver con el consumo de datos contextuales, que las comónadas pueden modelar explícitamente.
Técnicamente, una comonada es el dual categórico de una mónada, lo que significa que tendrá los mismos componentes requeridos, solo que con la dirección de las firmas de tipo invertidas . A partir de la definición de mónada centrada en bind , una comonada consta de:
cuenta(wa) : WT → T
=>>
) que extiende una cadena de funciones reductoras:(wa =>> f) : (WU, WU → T) → WT [r]
extender y contar también deben satisfacer los duales de las leyes de la mónada:
unidad ∘ ( (wa =>> f) → wb ) ↔ f(wa) → bwa =>> cuenta ↔ wawa ( (=>> f(wx = wa)) → wb (=>> g(wy = wb)) → wc ) ↔ ( wa (=>> f(wx = wa)) → wb ) (=>> g(wy = wb)) → wc
De manera análoga a las mónadas, las comonadas también pueden derivarse de funtores usando un dual de join :
duplicado(wa): WT → W (WT)
Sin embargo, si bien las operaciones como extend se invierten, una comónada no invierte las funciones sobre las que actúa y, en consecuencia, las comónadas siguen siendo funtores con map , no cofuntores . La definición alternativa con duplicate , counit y map también debe respetar sus propias leyes de comónadas:
((mapa duplicado) ∘ duplicado) wa ↔ (duplicado ∘ duplicado) wa ↔ wwwa((mapa cuenta) ∘ duplicado) wa ↔ (cuenta ∘ duplicado) wa ↔ wa((mapa mapa φ) ∘ duplicado) wa ↔ (duplicado ∘ (mapa φ)) wa ↔ wwb
Y al igual que con las mónadas, las dos formas se pueden convertir automáticamente:
(mapa φ) wa ↔ wa =>> (φ ∘ cuenta) wxduplicar wa ↔ wa =>> wx
wa =>> f(wx) ↔ ((mapa f) ∘ duplicado) wa
Un ejemplo simple es la comonada Product , que genera valores basados en un valor de entrada y datos de entorno compartidos. De hecho, la Product
comonada es simplemente el dual de la Writer
mónada y efectivamente lo mismo que la Reader
mónada (ambas se analizan a continuación) Product
y Reader
difieren solo en las firmas de función que aceptan y en cómo complementan esas funciones envolviendo o desenvolviendo valores.
Un ejemplo menos trivial es la comónada Stream , que se puede utilizar para representar flujos de datos y adjuntar filtros a las señales entrantes con extend . De hecho, aunque no son tan populares como las mónadas, los investigadores han descubierto que las comónadas son particularmente útiles para el procesamiento de flujos y la programación de modelado de flujos de datos . [35] [36]
Sin embargo, debido a sus definiciones estrictas, no es posible mover objetos de un lado a otro entre mónadas y comónadas. Como una abstracción aún mayor, las flechas pueden subsumir ambas estructuras, pero encontrar formas más granulares de combinar código monádico y comónado es un área activa de investigación. [37] [38]
Alternativas para los cálculos de modelado:
Conceptos de diseño relacionados:
Generalizaciones de las mónadas:
bind
que cuando se le da un tipo a que puede fallar y una asignación a → b que puede fallar, produce un resultado b que puede fallar. (Hutton, 2016) [7] lift
, junto con múltiples versiones para diferentes cantidades de parámetros, un detalle que se ignora aquí.Identity
mónada también puede considerarse como algo que surge de la adjunción de cualquier funtor con su inverso.bind
ha pegado en a string
donde anteriormente sólo integer
había an ; es decir, el programador ha construido un adjunto : una tupla (x,s)
, denotada int * string
en el pseudocódigo § anterior.{{cite journal}}
: CS1 maint: DOI inactive as of November 2024 (link)Referencias de HaskellWiki:
Tutoriales:
Probability
mónada para cadenas de Markov .Casos interesantes: