Se compararmos os tipos
(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m => m s -> (s -> m t) -> m t
temos uma pista do que separa os dois conceitos. Isso (s -> m t)
no tipo de (>>=)
mostra que um valor em s
pode determinar o comportamento de um cálculo em m t
. Mônadas permitem interferência entre as camadas de valor e computação. O (<*>)
operador não permite essa interferência: os cálculos da função e do argumento não dependem de valores. Isso realmente incomoda. 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 usa o resultado de algum efeito para decidir entre dois cálculos (por exemplo, lançar mísseis e assinar um armistício), enquanto
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 usa o valor de ab
para escolher entre os valores de dois cálculos at
e af
, tendo realizado ambos, talvez com efeito trágico.
A versão monádica depende essencialmente do poder extra de (>>=)
escolher um cálculo a partir de um valor, e isso pode ser importante. No entanto, apoiar esse poder torna as mônadas difíceis de compor. Se tentarmos construir 'double-bind'
(>>>>==) :: (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 ???
chegamos até aqui, mas agora nossas camadas estão todas misturadas. Temos um n (m (n t))
, então precisamos nos livrar do exterior n
. Como diz Alexandre C, podemos fazer isso se tivermos um adequado
swap :: n (m t) -> m (n t)
permutar o n
interior e join
o outro n
.
O mais fraco 'dupla aplicação' é muito mais fácil de definir
(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs
porque não há interferência entre as camadas.
Da mesma forma, é bom reconhecer quando você realmente precisa da potência extra de Monad
s e quando pode se safar com a estrutura de computação rígida que o Applicative
suporta.
Observe, a propósito, que embora compor mônadas seja difícil, pode ser mais do que você precisa. O tipo m (n v)
indica computação com m
-effects e, em seguida, computação com n
-effects para um -value v
, em que m
-effects termina antes de n
-effects iniciar (daí a necessidade de swap
). Se você deseja apenas intercalar m
-effects com n
-effects, então composição talvez seja pedir demais!