Diferença entre Monad e Applicative em Haskell

Acabei de ler o seguinte da typeclassopedia sobre a diferença entre Monad e Applicative . Eu posso entender que não há join no Applicative . Mas a descrição a seguir parece vaga para mim e não consegui descobrir o que exatamente significa “o resultado” de uma computação / ação monádica. Então, se eu colocar um valor em Maybe , que faz uma mônada, qual é o resultado dessa “computação”?

Vamos ver mais de perto o tipo de (>> =). A intuição básica é que combina dois cálculos em uma computação maior. O primeiro argumento, ma, é o primeiro cálculo. No entanto, seria chato se o segundo argumento fosse apenas um mb; então não haveria maneira de os cálculos interagirem uns com os outros (na verdade, essa é exatamente a situação com o Applicative). Portanto, o segundo argumento para (>> =) tem um tipo a -> mb: uma function desse tipo, dado um resultado da primeira computação, pode produzir uma segunda computação a ser executada. … Intuitivamente, é essa capacidade de usar a saída de cálculos anteriores para decidir quais cálculos serão executados a seguir, o que torna a Monad mais poderosa que a Applicative. A estrutura de uma computação Aplicativa é fixa, enquanto a estrutura de uma computação de Mônadas pode mudar com base em resultados intermediários.

Existe um exemplo concreto que ilustra a “capacidade de usar a saída de cálculos anteriores para decidir quais cálculos serão executados a seguir”, que a Applicative não possui?

Meu exemplo favorito é o “puramente aplicativo Either”. Começaremos analisando a instância de Monad base para

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

Esta instância incorpora uma noção muito natural de curto-circuito: nós procedemos da esquerda para a direita e uma vez que uma única computação “falha” na Left então todos os outros também. Há também a instância natural de Applicative que qualquer Monad tem

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

onde ap nada mais é do que seqüenciamento da esquerda para a direita antes de um return :

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

Agora, o problema com essa instância aparece quando você deseja coletar mensagens de erro que ocorrem em qualquer parte de uma computação e, de alguma forma, produzem um resumo dos erros. Isso voa em face de curto-circuito. Ele também voa em face do tipo de (>>=)

 (>>=) :: ma -> (a -> mb) -> mb 

Se pensarmos em ma como "o passado" e mb como "o futuro", então (>>=) produz o futuro do passado, desde que ele possa executar o "stepper" (a -> mb) . Este "stepper" exige que o valor de a realmente exista no futuro ... e isso é impossível para Either . Portanto (>>=) exige curto-circuito.

Então, em vez disso, implementaremos uma instância de Applicative que não pode ter uma Monad correspondente.

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

Agora a implementação de (<*>) é a parte especial que vale a pena considerar com cuidado. Ele executa uma certa quantidade de "curto-circuito" em seus 3 primeiros casos, mas faz algo interessante no quarto.

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

Observe novamente que, se pensarmos no argumento da esquerda como "o passado" e o argumento correto como "o futuro", então (<*>) é especial comparado a (>>=) , pois é permitido "abrir" o futuro e o passado em paralelo, em vez de necessitar necessariamente de resultados do "passado" para computar "o futuro".

Isto significa, diretamente, que podemos usar nosso puramente Applicative para coletar erros, ignorando os S da Right se algum Left existir na cadeia.

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

Então vamos inverter essa intuição em sua cabeça. O que não podemos fazer com um aplicativo puramente? Bem, desde que sua operação depende de examinar o futuro antes de executar o passado, devemos ser capazes de determinar a estrutura do futuro sem depender de valores no passado. Em outras palavras, não podemos escrever

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

que satisfaz as seguintes equações

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

enquanto podemos escrever ifM

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

de tal modo que

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

Essa impossibilidade surge porque o ifA incorpora exatamente a idéia do cálculo do resultado, dependendo dos valores incorporados nos cálculos do argumento.

Just 1 descreve uma “computação”, cujo “resultado” é 1. Nothing descreve uma computação que não produz resultados.

A diferença entre uma Mônada e um Aplicativo é que na Mônada há uma escolha. A principal distinção do Monads é a capacidade de escolher entre diferentes caminhos na computação (não apenas sair cedo). Dependendo de um valor produzido por uma etapa anterior na computação, o restante da estrutura de cálculo pode mudar.

Veja o que isso significa. Na cadeia monádica

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

o if escolhe qual cálculo construir.

No caso de Requerente, em

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

todas as funções funcionam com cálculos “internos”, não há chance de quebrar uma cadeia. Cada function apenas transforma um valor que é alimentado. A “forma” da estrutura de cálculo é inteiramente “do lado de fora” do ponto de vista das funções.

Uma function pode retornar um valor especial para indicar falha, mas não pode fazer com que as próximas etapas da computação sejam ignoradas. Todos eles terão que processar o valor especial de maneira especial também. A forma do cálculo não pode ser alterada de acordo com o valor recebido.

Com as mônadas, as próprias funções constroem cálculos para sua escolha.

Aqui está a minha opinião sobre @J. O exemplo de Abrahamson sobre o porquê ifA não pode usar o valor dentro de, por exemplo (pure True) . Em essência, ainda se resume à ausência da function de join de Monad em Applicative , que unifica as duas outlook diferentes dadas em typeclassopedia para explicar a diferença entre Monad e Applicative .

Então, usando @J. O exemplo de Abrahamson de aplicação puramente

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

(que tem efeito de curto-circuito similar ao Either Monad ) e a function ifA

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

E se tentarmos alcançar as equações mencionadas:

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

?

Bem, como já foi dito, em última análise, o conteúdo de (pure True) , não pode ser usado por uma computação posterior. Mas tecnicamente falando, isso não está certo. Podemos usar o conteúdo de (pure True) já que uma Monad também é um Functor com fmap . Nós podemos fazer:

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

O problema é com o tipo de retorno de ifA' , que é f (fa) . No Applicative , não há como recolocar dois Applicative S nesteds em um. Mas essa function de colapso é precisamente o que a join no Monad realiza. Assim,

 ifA = join . ifA' 

irá satisfazer as equações para ifA , se pudermos implementar a join apropriadamente. O Applicative está faltando aqui é exatamente a function de join . Em outras palavras, podemos de alguma forma usar o resultado do resultado anterior em Applicative . Mas fazê-lo em uma estrutura de Applicative envolverá o aumento do tipo de valor de retorno para um valor de aplicativo nested, que não temos como trazer de volta a um valor de aplicativo de nível único. Este será um problema sério porque, por exemplo, não podemos compor funções usando o Applicative S apropriadamente. O uso do join resolve o problema, mas a própria introdução do join promove o Applicative para um Monad .

A chave da diferença pode ser observada no tipo de ap vs type de =<< .

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

Em ambos os casos há ma , mas somente no segundo caso ma pode decidir se a function (a->mb) é aplicada. Por sua vez, a function (a->mb) pode "decidir" se a próxima function é aplicada - produzindo um mb que não "contenha" b (como [] , Nothing ou Left ).

No Applicative não há como as funções "dentro" m (a->b) tomarem tais "decisões" - elas sempre produzem um valor do tipo b .

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

Em Applicative isso não é possível, então não pode mostrar um exemplo. O mais próximo é:

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

Mas a descrição a seguir parece vaga para mim e não consegui descobrir o que exatamente significa “o resultado” de uma computação / ação monádica.

Bem, essa imprecisão é um tanto deliberada, porque o que “o resultado” é de uma computação monádica é algo que depende de cada tipo. A melhor resposta é um pouco tautológica: o “resultado” (ou resultados , já que pode haver mais de um) é qualquer valor (es) que a implementação da instância de (>>=) :: Monad m => ma -> (a -> mb) -> mb invoca o argumento da function com.

Então, se eu colocar um valor em Maybe , que faz uma mônada, qual é o resultado dessa “computação”?

The Maybe monad parece com isso:

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

A única coisa aqui qualificada como “resultado” é o a na segunda equação de >>= , porque é a única coisa que é “alimentada” para o segundo argumento de >>= .

Outras respostas foram aprofundadas sobre a diferença ifM vs. ifM , então eu pensei em destacar outra diferença significativa: os aplicativos compõem, as mônadas não . Com o Monad s, se você quiser fazer um Monad que combine os efeitos de dois existentes, você terá que rewrite um deles como um transformador de mônada. Por outro lado, se você tiver dois Applicatives você pode facilmente fazer um deles mais complexo, como mostrado abaixo. (O código é copiado dos transformers .)

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

Agora, se adicionarmos no functor / aplicativo Constant :

 newtype Constant ab = 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 montar o “aplicativo” das outras respostas de Lift e Constant :

 type Error ea = Lift (Constant e) a 
    Intereting Posts