math - traduccion - ¿Qué tiene de especial Monads?
monad traduccion (5)
Una mónada es una estructura matemática que se usa mucho en la programación funcional (pura), básicamente Haskell. Sin embargo, hay muchas otras estructuras matemáticas disponibles, como por ejemplo funcionadores aplicativos, mónadas fuertes o monoides. Algunos tienen más específicos, algunos son más genéricos . Sin embargo, las mónadas son mucho más populares. ¿Porqué es eso?
Una explicación que se me ocurrió, es que son un punto dulce entre la genicidad y la especificidad. Esto significa que las mónadas capturan suposiciones suficientes sobre los datos para aplicar los algoritmos que normalmente utilizamos y los datos que usualmente tenemos cumplen con las leyes monádicas.
Otra explicación podría ser que Haskell proporciona sintaxis para las mónadas (notación do), pero no para otras estructuras, lo que significa que los programadores Haskell (y por lo tanto los investigadores de programación funcional) son atraídos intuitivamente hacia las mónadas, donde una función más genérica o específica (eficiente) trabajo tambien
Bueno, primero déjame explicarte cuál es el papel de las mónadas: las mónadas son muy poderosas, pero en cierto sentido: puedes expresar cualquier cosa usando una mónada. Haskell como un lenguaje no tiene cosas como bucles de acción, excepciones, mutación, goto, etc. Las mónadas se pueden expresar dentro del lenguaje (para que no sean especiales) y hacer que todo esto sea alcanzable.
Esto tiene un lado positivo y negativo: es positivo que puedas expresar todas esas estructuras de control que conoces de la programación imperativa y muchas de ellas no. Recientemente desarrollé una mónada que le permite volver a ingresar en un cálculo en algún lugar en el medio con un contexto ligeramente modificado. De esta forma puede ejecutar un cálculo, y si falla, simplemente intente de nuevo con valores ligeramente ajustados. Además, las acciones monádicas son de primera clase, y así es como se construyen cosas como bucles o manejo de excepciones. Mientras que while
es primitivo en C en Haskell, en realidad es solo una función regular.
El lado negativo es que las mónadas no te dan prácticamente ninguna garantía. Son tan poderosos que se te permite hacer lo que quieras, para decirlo simplemente. En otras palabras, al igual que sabes de los lenguajes imperativos, puede ser difícil razonar sobre el código con solo mirarlo.
Las abstracciones más generales son más generales en el sentido de que permiten que se expresen algunos conceptos que no se pueden expresar como mónadas. Pero eso es solo una parte de la historia. Incluso para las mónadas, puede utilizar un estilo conocido como estilo aplicativo , en el que utiliza la interfaz de aplicación para componer su programa a partir de piezas aisladas pequeñas. El beneficio de esto es que puede razonar sobre el código con solo mirarlo y puede desarrollar componentes sin tener que prestar atención al resto de su sistema.
Primero, creo que no es del todo cierto que las mónadas sean mucho más populares que cualquier otra cosa; tanto Functor como Monoid tienen muchas instancias que no son mónadas. Pero ambos son muy específicos; Functor proporciona mapeo, concatenación Monoid. Aplicativo es la única clase en la que puedo pensar que probablemente se infrautilice dado su considerable poder, debido en gran parte a que es una adición relativamente reciente al lenguaje.
Pero sí, las mónadas son extremadamente populares. Parte de eso es la notación do; muchos Monoids proporcionan instancias de Monad que simplemente agregan valores a un acumulador en ejecución (esencialmente un escritor implícito). La biblioteca blaze-html es un buen ejemplo. La razón, creo, es el poder de la firma de tipo (>>=) :: Monad m => ma -> (a -> mb) -> mb
. Si bien fmap y mappend son útiles, lo que pueden hacer es restringir de forma bastante restringida. bind, sin embargo, puede expresar una gran variedad de cosas. Es, por supuesto, canonizado en la mónada IO, quizás el mejor enfoque funcional puro para IO antes de las transmisiones y FRP (y aún útil junto a ellas para tareas simples y componentes de definición). Pero también proporciona un estado implícito (Reader / Writer / ST), que puede evitar el paso de una variable muy tediosa. Las diversas mónadas de estados, especialmente, son importantes porque proporcionan una garantía de que el estado es de un solo hilo, lo que permite estructuras mutables en código puro (sin IO) antes de la fusión. Pero bind tiene algunos usos más exóticos, como el aplanamiento de las estructuras de datos anidados (las mónadas List and Set), que son bastante útiles en su lugar (y generalmente las veo desajustadas, llamando a liftM o (>> =) explícitamente, así que no es una cuestión de hacer notación). Por lo tanto, mientras Functor y Monoid (y el algo más raro Plegable, Alternativo, Traversable y otros) proporcionan una interfaz estandarizada para una función bastante sencilla, el enlace de Monad es considerablemente más flexible.
En resumen, creo que todas tus razones tienen algún papel; la popularidad de las mónadas se debe a una combinación de accidentes históricos (notación do y definición tardía de Aplicativo) y su combinación de poder y generalidad (en relación con funtores, monoides y similares) y comprensibilidad (relativa a las flechas).
Las mónadas son especiales debido a la notación do
, que le permite escribir programas imperativos en un lenguaje funcional. La mónada es la abstracción que le permite empalmar los programas imperativos con componentes pequeños y reutilizables (que a su vez son programas imperativos). Los transformadores Monad son especiales porque representan la mejora de un lenguaje imperativo con nuevas características.
Si un tipo m :: * -> *
tiene una instancia de Monad
, obtienes la composición completa de las funciones de Turing con el tipo a -> mb
. Esta es una propiedad fantásticamente útil. Obtiene la capacidad de abstraer varios flujos de control completos de Turing lejos de significados específicos. Es un patrón de composición mínima que permite abstraer cualquier flujo de control para trabajar con tipos que lo soportan.
Compare esto con Applicative
, por ejemplo. Ahí, obtienes solo patrones de composición con poder computacional equivalente a un autómata push-down. Por supuesto, es cierto que hay más tipos que admiten composición con una potencia más limitada. Y es verdad que cuando limita la potencia disponible, puede hacer optimizaciones adicionales. Estas dos razones explican por qué existe la clase Applicative
y es útil. Pero las cosas que pueden ser instancias de Monad
suelen ser, para que los usuarios del tipo puedan realizar las operaciones más generales posibles con el tipo.
Editar: Por demanda popular, aquí hay algunas funciones que usan la clase Monad
:
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= /z -> if z then x else y
whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)
(*&&) :: Monad m => m Bool -> m Bool -> m Bool
x *&& y = ifM x y (return False)
(*||) :: Monad m => m Bool -> m Bool -> m Bool
x *|| y = ifM x (return True) y
notM :: Monad m => m Bool -> m Bool
notM x = x >>= return . not
La combinación de aquellos con la sintaxis do (o el operador raw >>=
) le da vinculación de nombre, bucle indefinido y completa lógica booleana. Es un conjunto bien conocido de primitivos suficientes para proporcionarle a Turing integridad. Observe cómo se han levantado todas las funciones para trabajar en valores monádicos, en lugar de valores simples. Todos los efectos monádicos están vinculados solo cuando es necesario; solo los efectos de la rama elegida de ifM
están vinculados a su valor final. Both *&&
y *||
ignore su segundo argumento cuando sea posible. Y así..
Ahora, esas firmas de tipo pueden no involucrar funciones para cada operando monádico, pero eso es solo una simplificación cognitiva. No habría diferencia semántica, ignorando fondos, si todos los argumentos y resultados fuera de función se cambiaran a () -> ma
. Es simplemente más amigable para los usuarios optimizar el gasto cognitivo.
Ahora, veamos qué sucede con esas funciones con la interfaz Applicative
.
ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (/c'' x'' y'' -> if c'' then x'' else y'') <$> c <*> x <*> y
Bueno, uh. Tiene el mismo tipo de firma. Pero ya hay un gran problema aquí. Los efectos tanto de x como de y están ligados a la estructura compuesta, independientemente de cuál sea el valor seleccionado.
whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <$> step x) (pure x)
Bueno, vale, parece que estaría bien, excepto por el hecho de que es un ciclo infinito porque ifA
siempre ejecutará ambas ramas ... Excepto que ni siquiera está tan cerca. pure x
tiene el tipo fa
. whileA p step <$> step x
tiene el tipo f (fa)
. Esto ni siquiera es un ciclo infinito. Es un error de compilación. Intentemoslo de nuevo..
whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)
Bueno disparar Ni siquiera llegues tan lejos. whileA p step
tiene el tipo a -> fa
. Si intenta usarlo como primer argumento para <*>
, toma la instancia Applicative
para el constructor de tipo superior , que es (->)
, no f
. Sí, esto tampoco va a funcionar.
De hecho, la única función de mis ejemplos de Monad
que funcionaría con la interfaz Applicative
es notM
. Esa función particular funciona muy bien solo con una interfaz Functor
, de hecho. ¿El resto? Ellos fallan.
Por supuesto, es de esperar que pueda escribir código utilizando la interfaz Monad
que no puede con la interfaz Applicative
. Es estrictamente más poderoso, después de todo. Pero lo interesante es lo que pierdes. Pierdes la capacidad de componer funciones que cambian los efectos que tienen en función de su entrada. Es decir, pierde la capacidad de escribir ciertos patrones de flujo de control que componen funciones con tipos a -> fb
.
La composición completa de Turing es exactamente lo que hace que la interfaz de Monad
interesante. Si no permitía la composición completa de Turing, sería imposible para usted, el programador, componer juntas las acciones de IO
en cualquier flujo de control particular que no estuviese preempaquetado para usted. Fue el hecho de que puede usar las primitivas de Monad
para expresar cualquier flujo de control que hizo que el tipo IO
sea una forma factible de manejar el problema de E / S en Haskell.
Muchos más tipos que solo IO
tienen interfaces Monad
semánticamente válidas. Y sucede que Haskell tiene las funciones de idioma para abstraerse en toda la interfaz. Debido a esos factores, Monad
es una clase valiosa para proporcionar instancias, cuando sea posible. Al hacerlo, tendrá acceso a todas las funcionalidades abstractas existentes para trabajar con tipos monádicos, independientemente de cuál sea el tipo concreto.
Entonces, si los programadores de Haskell parecen preocuparse por las instancias de Monad
para un tipo, es porque es la instancia más genéricamente útil que se puede proporcionar.
Sospecho que la atención desproporcionadamente grande dada a esta clase de tipo particular ( Monad
) sobre muchas otras es principalmente un golpe de suerte histórico. Las personas a menudo asocian IO
con Monad
, aunque las dos son independientemente ideas útiles ( como son reversión de listas y plátanos ). Debido a que IO
es mágico (tiene una implementación pero no denotación) y Monad
menudo se asocia con IO
, es fácil caer en el pensamiento mágico sobre Monad
.
(Aparte: es cuestionable si IO
incluso es una mónada. ¿Las leyes de la mónada se mantienen? ¿Qué significan las leyes para IO
, es decir, qué significa la igualdad? Tenga en cuenta la asociación problemática con la mónada de estado .)