Los candidatos componen, las mónadas no

110

Los candidatos componen, las mónadas no.

¿Qué significa la declaración anterior? ¿Y cuándo es preferible uno a otro?

desaparecido faktor
fuente
5
¿De dónde sacaste esta declaración? Puede ser útil ver algo de contexto.
fuz
@FUZxxl: Lo he escuchado repetidamente de muchas personas diferentes, recientemente de debasishg en Twitter.
missingfaktor
3
@stephen tetley: Tenga en cuenta que muchos de estos Applicatives son en realidad una familia completa de Monads, es decir, uno para cada "forma" de estructura posible. ZipListno es a Monad, pero ZipLists de longitud fija son. Readeres un caso especial conveniente (¿o es general?) donde el tamaño de la "estructura" se fija como la cardinalidad del tipo de entorno.
CA McCann
3
@CAMcCann Todos esos aplicativos rápidos (ya sea que se trunquen o rellenen) se restringen a las mónadas si arreglas la forma de una manera que equivale a una Readermónada hasta un isomorfismo. Una vez que fija la forma de un contenedor, codifica efectivamente una función a partir de posiciones, como un memo trie. Peter Hancock llama a estos functores "naperianos", ya que obedecen las leyes de los logaritmos.
trabajador porcino
4
@stephen tetley: Otros ejemplos incluyen el aplicativo de monoide constante (que es una composición de mónadas pero no una mónada) y el aplicativo de retraso unitario (que es mejor no admitir la unión).
trabajador porcino

Respuestas:

115

Si comparamos los tipos

(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m =>       m s -> (s -> m t) -> m t

tenemos una pista de lo que separa los dos conceptos. Que (s -> m t)en el tipo de (>>=)muestra que un valor en spuede determinar el comportamiento de un cálculo en m t. Las mónadas permiten la interferencia entre las capas de valor y de cálculo. El (<*>)operador no permite tal interferencia: los cálculos de la función y el argumento no dependen de los valores. Esto realmente muerde. Comparar

miffy :: Monad m => m Bool -> m x -> m x -> m x
miffy mb mt mf = do
  b <- mb
  if b then mt else mf

que utiliza el resultado de algún efecto para decidir entre dos cálculos (por ejemplo, lanzar misiles y firmar un armisticio), mientras que

iffy :: Applicative a => a Bool -> a x -> a x -> a x
iffy ab at af = pure cond <*> ab <*> at <*> af where
  cond b t f = if b then t else f

que utiliza el valor de abpara elegir entre los valores de dos cálculos aty af, habiendo realizado ambos, quizás con efecto trágico.

La versión monádica se basa esencialmente en el poder adicional de (>>=)elegir un cálculo a partir de un valor, y eso puede ser importante. Sin embargo, apoyar ese poder hace que las mónadas sean difíciles de componer. Si intentamos construir 'doble vínculo'

(>>>>==) :: (Monad m, Monad n) => m (n s) -> (s -> m (n t)) -> m (n t)
mns >>>>== f = mns >>-{-m-} \ ns -> let nmnt = ns >>= (return . f) in ???

llegamos hasta aquí, pero ahora nuestras capas están revueltas. Tenemos un n (m (n t)), así que tenemos que deshacernos del exterior n. Como dice Alexandre C, podemos hacer eso si tenemos un

swap :: n (m t) -> m (n t)

permutar el ninterior y joinel otro n.

La 'doble aplicación' más débil es mucho más fácil de definir

(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs

porque no hay interferencia entre las capas.

En consecuencia, es bueno reconocer cuándo realmente necesita la potencia adicional de Monads, y cuándo puede salirse con la suya con la rígida estructura de cálculo que Applicativeadmite.

Tenga en cuenta, por cierto, que aunque componer mónadas es difícil, puede ser más de lo que necesita. El tipo m (n v)indica computación con m-effects, luego computación con n-effects a un v-valor, donde los m-effects terminan antes de que ncomiencen los -effects (de ahí la necesidad de swap). Si solo desea intercalar m-efectos con n-efectos, entonces la composición es quizás demasiado pedir.

trabajador porcino
fuente
3
Para el ejemplo dudoso, usted afirma que "utiliza el valor de ab para elegir entre los valores de dos cálculos at y af, habiendo realizado ambos, quizás con un efecto trágico". ¿No te protege la naturaleza perezosa de Haskell contra esto? Si tengo list = (\ btf -> if b entonces t else f): [] y luego ejecute la instrucción: list <*> pure True <*> pure "hola" <*> pure (error "malo"). ... Recibo "hola" y el error nunca ocurre. Este código no es tan seguro o controlado como una mónada, pero parece que la publicación sugiere que los aplicativos causan una evaluación estricta. ¡Sin embargo, una gran publicación en general! ¡Gracias!
shj
7
Aún obtiene los efectos de ambos, pero puro (error "malo") no tiene ninguno. Si, por otro lado, intentas iffy (puro verdadero) (puro "hola") (error "malo"), obtienes un error que miffy evita. Además, si prueba algo como iffy (puro verdadero) (puro 0) [1,2], obtendrá [0,0] en lugar de [0]. Los aplicativos tienen una especie de rigor sobre ellos, en el sentido de que construyen secuencias fijas de cálculos, pero los valores resultantes de esos cálculos todavía se combinan perezosamente, como puede observar.
trabajador porcino
Es cierto, que para cualquier mónadas my nsiempre se puede escribir un transformador mónada mt, y operar en n (m t)el uso mt n t? Entonces, ¿siempre puedes componer mónadas, es más complicado, usar transformadores?
ron
4
Estos transformadores a menudo existen, pero hasta donde yo sé, no hay una forma canónica de generarlos. A menudo existe una elección genuina sobre cómo resolver los efectos intercalados de las diferentes mónadas, siendo el ejemplo clásico las excepciones y el estado. ¿Debe una excepción revertir los cambios de estado o no? Ambas opciones tienen su lugar. Habiendo dicho eso, hay una cosa de "mónada libre" que expresa "entrelazado arbitrario". data Free f x = Ret x | Do (f (Free f x)), entonces data (:+:) f g x = Inl (f x) | Tnr (g x), y considere Free (m :+: n). Eso retrasa la elección de cómo ejecutar intercalados.
trabajador porcino
@pigworker Sobre el debate perezoso / estricto. Creo que con los aplicativos no se puede controlar el efecto desde dentro del cálculo, pero la capa de efectos puede muy bien decidir no evaluar valores posteriores. Para los analizadores (aplicativos), esto significa que si el analizador falla antes, los analizadores posteriores no se evalúan / aplican a la entrada. Porque Maybeesto significa que un temprano Nothingsuprimirá la evaluación ade un posterior / posterior Just a. ¿Es esto correcto?
ziggystar
75

Los candidatos componen, las mónadas no.

Mónadas hacen componer, pero el resultado podría no ser una mónada. Por el contrario, la composición de dos aplicativos es necesariamente un aplicativo. Sospecho que la intención de la declaración original era que "la aplicabilidad compone, mientras que la mónada no". Reformulado, " Applicativeestá cerrado bajo composición y Monadno lo es".

Conal
fuente
24
Además, dos aplicativos cualesquiera se componen de manera completamente mecánica, mientras que la mónada formada por la composición de dos mónadas es específica de esa composición.
Apocalisp
12
Además, las mónadas se componen de otras formas, el producto de dos mónadas es una mónada, son solo los coproductos los que necesitan algún tipo de ley distributiva.
Edward KMETT
Con @Apocalisp, comentario incluido, esta es la mejor y más concisa respuesta.
Paul Draper
39

Si tiene aplicativos A1y A2, entonces el tipo data A3 a = A3 (A1 (A2 a))también es aplicativo (puede escribir tal instancia de forma genérica).

Por otro lado, si usted tiene mónadas M1y M2entonces el tipo data M3 a = M3 (M1 (M2 a))no es necesariamente una mónada (no hay una implementación genérica sensato >>=o joinde la composición).

Un ejemplo puede ser el tipo [Int -> a](aquí componimos un constructor de tipos []con (->) Int, los cuales son mónadas). Puedes escribir fácilmente

app :: [Int -> (a -> b)] -> [Int -> a] -> [Int -> b]
app f x = (<*>) <$> f <*> x

Y eso se generaliza a cualquier aplicativo:

app :: (Applicative f, Applicative f1) => f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)

Pero no existe una definición sensata de

join :: [Int -> [Int -> a]] -> [Int -> a]

Si no está convencido de esto, considere esta expresión:

join [\x -> replicate x (const ())]

La longitud de la lista devuelta se debe establecer en piedra antes de que se proporcione un número entero, pero la longitud correcta depende del número entero que se proporcione. Por tanto, no joinpuede existir una función correcta para este tipo.

Rotsor
fuente
1
... así que evita las mónadas cuando una función sirve?
Andrew Cooke
2
@andrew, si te refieres a functor, entonces sí, los functors son más simples y deben usarse cuando sea suficiente. Tenga en cuenta que no siempre es así. Por ejemplo, IOsin un Monadsería muy difícil de programar. :)
Rotsor
17

Desafortunadamente, nuestro objetivo real, la composición de mónadas, es bastante más difícil. .. De hecho, podemos probar que, en cierto sentido, no hay forma de construir una función de unión con el tipo anterior usando solo las operaciones de las dos mónadas (ver el apéndice para un esquema de la demostración). De ello se deduce que la única forma en que podríamos esperar formar una composición es si hay algunas construcciones adicionales que unan los dos componentes.

Composición de mónadas, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf

Landei
fuente
4
Tl; dr para lectores impacientes: puedes componer mónadas si (f?) Puedes proporcionar una transformación naturalswap : N M a -> M N a
Alexandre
@Alexandre C .: Solo "si", sospecho. No todos los transformadores de mónada se describen por composición de functor directo. Por ejemplo, ContT r m ano es ni m (Cont r a)tampoco Cont r (m a), y StateT s m aes más o menos Reader s (m (Writer s a)).
CA McCann
@CA McCann: Parece que no puedo pasar de (M mónada, N mónada, MN mónada, NM mónada) a (existe intercambio: MN -> NM natural). Así que sigamos con "si" por ahora (quizás la respuesta esté en el periódico, debo confesar que lo busqué rápidamente)
Alexandre C.
1
@Alexandre C .: De todos modos, es posible que solo especificar que las composiciones son mónadas no sea suficiente; también necesitas alguna forma de relacionar las dos partes con el todo. La existencia de swapimplica que la composición permite a los dos "cooperar" de alguna manera. Además, tenga en cuenta que sequencees un caso especial de "intercambio" para algunas mónadas. En fliprealidad, también lo es.
CA McCann
7
Para escribir swap :: N (M x) -> M (N x), me parece que puede usar returns(adecuadamente fmapped) para insertar un Men el frente y un Nen la parte posterior, yendo desde N (M x) -> M (N (M (N x))), luego use el joindel compuesto para obtener su M (N x).
trabajador porcino
7

La solución de la ley distributiva l: MN -> NM es suficiente

para garantizar la monadicidad de NM. Para ver esto necesitas una unidad y un mult. Me enfocaré en el mult (la unidad es unit_N unitM)

NMNM - l -> NNMM - mult_N mult_M -> NM

Esto no garantiza que MN sea una mónada.

Sin embargo, la observación crucial entra en juego cuando tiene soluciones de ley distributiva

l1 : ML -> LM
l2 : NL -> LN
l3 : NM -> MN

por tanto, LM, LN y MN son mónadas. Surge la pregunta de si LMN es una mónada (ya sea por

(MN) L -> L (MN) o por N (LM) -> (LM) N

Tenemos suficiente estructura para hacer estos mapas. Sin embargo, como observa Eugenia Cheng , necesitamos una condición hexagonal (que equivale a una presentación de la ecuación de Yang-Baxter) para garantizar la monadicidad de cualquier construcción. De hecho, con la condición hexagonal, las dos mónadas diferentes coinciden.

usuario278559
fuente
9
Esta es probablemente una gran respuesta, pero fue zas manera sobre mi cabeza.
Dan Burton
1
Esto se debe a que, al usar el término Aplicativo y etiqueta Haskell, esta es una pregunta sobre Haskell pero con una respuesta en una notación diferente.
codeshot