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.