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 s
puede 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 ab
para elegir entre los valores de dos cálculos at
y 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 n
interior y join
el 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 Monad
s, y cuándo puede salirse con la suya con la rígida estructura de cálculo que Applicative
admite.
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 n
comiencen 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.