mónada monadologia monadas leibniz las filosofía filosofia educatina doctrina haskell monads applicative

haskell - monadologia - monadas filosofia



Diferencia entre Mónada y Aplicativo en Haskell (5)

Pero la siguiente descripción me parece vaga y no pude entender qué significa exactamente "el resultado" de una computación / acción monádica.

Bueno, esa vaguedad es algo deliberada, porque lo que "el resultado" es de un cálculo monádico es algo que depende de cada tipo. La mejor respuesta es un poco tautológica: el "resultado" (o resultados , ya que puede haber más de uno) es cualquier valor que la implementación de la instancia de (>>=) :: Monad m => ma -> (a -> mb) -> mb invoca el argumento de función con.

Entonces, si pongo un valor en Maybe , que hace una mónada, ¿cuál es el resultado de este "cálculo"?

The Maybe Monada se ve así:

instance Monad Maybe where return = Just Nothing >>= _ = Nothing Just a >>= k = k a

Lo único aquí que califica como un "resultado" es la a en la segunda ecuación para >>= , porque es lo único que se "alimenta" al segundo argumento de >>= .

Otras respuestas han profundizado sobre la diferencia ifA vs. ifM , así que pensé que destacaría otra diferencia significativa: los ifA componen, las mónadas no . Con Monad s, si quiere hacer una Monad que combine los efectos de dos existentes, debe reescribir uno de ellos como un transformador de mónada. Por el contrario, si tiene dos Applicatives , puede crear fácilmente uno más complejo, como se muestra a continuación. (El código es copiado de los transformers )

-- | The composition of two functors. newtype Compose f g a = Compose { getCompose :: f (g a) } -- | The composition of two functors is also a functor. instance (Functor f, Functor g) => Functor (Compose f g) where fmap f (Compose x) = Compose (fmap (fmap f) x) -- | The composition of two applicatives is also an applicative. instance (Applicative f, Applicative g) => Applicative (Compose f g) where pure x = Compose (pure (pure x)) Compose f <*> Compose x = Compose ((<*>) <$> f <*> x) -- | The product of two functors. data Product f g a = Pair (f a) (g a) -- | The product of two functors is also a functor. instance (Functor f, Functor g) => Functor (Product f g) where fmap f (Pair x y) = Pair (fmap f x) (fmap f y) -- | The product of two applicatives is also an applicative. instance (Applicative f, Applicative g) => Applicative (Product f g) where pure x = Pair (pure x) (pure x) Pair f g <*> Pair x y = Pair (f <*> x) (g <*> y) -- | The sum of a functor @f@ with the ''Identity'' functor data Lift f a = Pure a | Other (f a) -- | The sum of two functors is always a functor. instance (Functor f) => Functor (Lift f) where fmap f (Pure x) = Pure (f x) fmap f (Other y) = Other (fmap f y) -- | The sum of any applicative with ''Identity'' is also an applicative instance (Applicative f) => Applicative (Lift f) where pure = Pure Pure f <*> Pure x = Pure (f x) Pure f <*> Other y = Other (f <$> y) Other f <*> Pure x = Other (($ x) <$> f) Other f <*> Other y = Other (f <*> y)

Ahora, si agregamos el functor Constant / aplicativo:

newtype Constant a b = Constant { getConstant :: a } instance Functor (Constant a) where fmap f (Constant x) = Constant x instance (Monoid a) => Applicative (Constant a) where pure _ = Constant mempty Constant x <*> Constant y = Constant (x `mappend` y)

... podemos armar el " Either aplicativo" de las otras respuestas de Lift y Constant :

type Error e a = Lift (Constant e) a

Acabo de leer lo siguiente de typeclassopedia sobre la diferencia entre Monad y Applicative . Puedo entender que no hay join en Applicative . Pero la siguiente descripción me parece vaga y no pude entender qué significa exactamente "el resultado" de una computación / acción monádica. Entonces, si pongo un valor en Maybe , que hace una mónada, ¿cuál es el resultado de este "cálculo"?

Miremos más de cerca el tipo de (>> =). La intuición básica es que combina dos cálculos en un cálculo más grande. El primer argumento, ma, es el primer cálculo. Sin embargo, sería aburrido si el segundo argumento fuera solo un mb; entonces no habría forma de que los cálculos interactúen entre sí (de hecho, esta es exactamente la situación con Applicative). Entonces, el segundo argumento para (>> =) tiene un tipo a -> mb: una función de este tipo, dado el resultado del primer cálculo, puede producir un segundo cálculo para ser ejecutado. ... Intuitivamente, es esta capacidad de usar la salida de cómputos previos para decidir qué cálculos ejecutar a continuación lo que hace que Monad sea más poderosa que Aplicable. La estructura de un cálculo Aplicativo es fija, mientras que la estructura de un cálculo de Monad puede cambiar en base a resultados intermedios.

¿Hay algún ejemplo concreto que ilustre la "capacidad de utilizar el resultado de los cálculos previos para decidir qué cálculos ejecutar a continuación", que el Aplicativo no tiene?


Aquí está mi opinión sobre @J. El ejemplo de Abrahamson de por qué ifA no puede usar el valor dentro, por ejemplo, (pure True) . En esencia, todavía se reduce a la ausencia de la función de join de Monad en Applicative , que unifica las dos perspectivas diferentes dadas en typeclassopedia para explicar la diferencia entre Monad y Applicative .

Entonces usando @J. El ejemplo de Abrahamson de puramente aplicativo Either :

instance Monoid e => Applicative (Either e) where pure = Right Right f <*> Right a = Right (f a) -- neutral Left e <*> Right _ = Left e -- short-circuit Right _ <*> Left e = Left e -- short-circuit Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!

(que tiene un efecto de cortocircuito similar al Either Monad ) y la función ifA

ifA :: Applicative f => f Bool -> f a -> f a -> f a

¿Qué pasa si tratamos de lograr las ecuaciones mencionadas?

ifA (pure True) t e == t ifA (pure False) t e == e

?

Bueno, como ya se señaló, en última instancia, el contenido de (pure True) no puede ser utilizado por un cálculo posterior. Pero técnicamente hablando, esto no está bien. Podemos usar el contenido de (pure True) ya que una Monad también es un fmap con fmap . Podemos hacer:

ifA'' b t e = fmap b (/x -> if x then t else e)

El problema es con el tipo de retorno de ifA'' , que es f (fa) . En Applicative , no hay forma de colapsar dos S Applicative anidado en uno. Pero esta función colapsable es precisamente lo que join en Monad . Asi que,

ifA = join . ifA''

satisfará las ecuaciones para ifA , si podemos implementar join adecuadamente. Lo que falta de Applicative aquí es exactamente la función de join . En otras palabras, de alguna manera podemos usar el resultado del resultado anterior en Applicative . Pero hacerlo en un marco de Applicative implicará aumentar el tipo del valor de retorno a un valor aplicativo anidado, que no tenemos medios para devolver a un valor aplicativo de un solo nivel. Este será un problema grave porque, por ejemplo, no podemos componer funciones utilizando Applicative S de manera adecuada. Usar join soluciona el problema, pero la misma introducción de join promueve el Applicative a una Monad .


La clave de la diferencia se puede observar en el tipo de ap vs tipo de =<< .

ap :: m (a->b) -> (m a->m b) =<< :: (a->m b) -> (m a->m b)

En ambos casos, hay ma , pero solo en el segundo caso ma puede decidir si se aplica la función (a->mb) . A su vez, la función (a->mb) puede "decidir" si se aplica la función siguiente: produciendo tal mb que no "contiene" b (como [] , Nothing o Left ).

En Applicative no hay forma de que las funciones "dentro" de m (a->b) tomen tales "decisiones", siempre producen un valor de tipo b .

f 1 = Nothing -- here f "decides" to produce Nothing f x = Just x Just 1 >>= f >>= g -- g doesn''t get applied, because f decided so.

En Applicative esto no es posible, por lo que no puede mostrar un ejemplo. Lo más cercano es:

f 1 = 0 f x = x g <$> f <$> Just 1 -- oh well, this will produce Just 0, but can''t stop g -- from getting applied


Mi ejemplo favorito es "cualquiera de los dos". Comenzaremos analizando la instancia base de Monad para cualquiera

instance Monad (Either e) where return = Right Left e >>= _ = Left e Right a >>= f = f a

Esta instancia incorpora una noción de cortocircuito muy natural: procedemos de izquierda a derecha y una vez que un solo cálculo "falla" en la Left , todos los demás también lo hacen. También existe la instancia Applicative natural que cualquier Monad tiene

instance Applicative (Either e) where pure = return (<*>) = ap

donde ap no es más que una secuencia de izquierda a derecha antes de una return :

ap :: Monad m => m (a -> b) -> m a -> m b ap mf ma = do f <- mf a <- ma return (f a)

Ahora, el problema con esta instancia se revela cuando desea recopilar mensajes de error que se producen en cualquier parte de un cálculo y, de alguna forma, producir un resumen de errores. Esto contradice el cortocircuito. También va en contra del tipo de (>>=)

(>>=) :: m a -> (a -> m b) -> m b

Si pensamos en ma como "el pasado" y mb como "el futuro", entonces (>>=) produce el futuro del pasado siempre que pueda ejecutar el "paso a paso" (a -> mb) . Este "paso a paso" exige que el valor de a realmente exista en el futuro ... y esto es imposible para Either . Por lo tanto, (>>=) exige un cortocircuito.

Entonces, implementaremos una instancia Applicative que no puede tener una Monad correspondiente.

instance Monoid e => Applicative (Either e) where pure = Right

Ahora la implementación de (<*>) es la parte especial que vale la pena considerar cuidadosamente. Realiza una cierta cantidad de "cortocircuitos" en sus primeros 3 casos, pero hace algo interesante en el cuarto.

Right f <*> Right a = Right (f a) -- neutral Left e <*> Right _ = Left e -- short-circuit Right _ <*> Left e = Left e -- short-circuit Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!

Observe nuevamente que si pensamos en el argumento de la izquierda como "el pasado" y el argumento de la derecha como "el futuro", entonces (<*>) es especial en comparación con (>>=) ya que se permite "abrir" el futuro y el pasado en paralelo en lugar de necesitar necesariamente resultados del "pasado" para calcular "el futuro".

Esto significa, directamente, que podemos usar nuestra puramente Applicative Either para recopilar errores, ignorando los Right si existen algunos de Left en la cadena.

> Right (+1) <*> Left [1] <*> Left [2] > Left [1,2]

Así que volvamos esta intuición en su cabeza. ¿Qué no podemos hacer con un aplicativo puramente? Bueno, dado que su operación depende de examinar el futuro antes de ejecutar el pasado, debemos ser capaces de determinar la estructura del futuro sin depender de los valores del pasado. En otras palabras, no podemos escribir

ifA :: Applicative f => f Bool -> f a -> f a -> f a

que satisface las siguientes ecuaciones

ifA (pure True) t e == t ifA (pure False) t e == e

mientras que podemos escribir ifM

ifM :: Monad m => m Bool -> m a -> m a -> m a ifM mbool th el = do bool <- mbool if bool then th else el

tal que

ifM (return True) t e == t ifM (return False) t e == e

Esta imposibilidad surge porque ifA encarna exactamente la idea del cálculo del resultado en función de los valores incrustados en los cálculos del argumento.


Just 1 describe un "cálculo", cuyo "resultado" es 1. Nothing describe un cálculo que no produce ningún resultado.

La diferencia entre una Mónada y un Aplicativo es que en la Mónada hay una opción. La distinción clave de las Mónadas es la capacidad de elegir entre diferentes caminos en el cálculo (no solo salir temprano). Dependiendo de un valor producido por un paso previo en el cálculo, el resto de la estructura de cálculo puede cambiar.

Esto es lo que esto significa. En la cadena monádica

return 42 >>= (/x -> if x == 1 then return (x+1) else return (x-1) >>= (/y -> return (1/y) ))

el if elige qué cálculo construir.

En caso de Applicative, en

pure (1/) <*> ( pure (+(-1)) <*> pure 1 )

todas las funciones funcionan cálculos "dentro", no hay posibilidad de romper una cadena. Cada función simplemente transforma un valor que se alimenta. La "forma" de la estructura de cálculo está completamente "en el exterior" desde el punto de vista de las funciones.

Una función podría devolver un valor especial para indicar la falla, pero no puede hacer que los siguientes pasos en el cálculo sean omitidos. Todos ellos tendrán que procesar el valor especial de una manera especial también. La forma del cálculo no se puede cambiar de acuerdo con el valor recibido.

Con las mónadas, las funciones mismas construyen cómputos a su elección.