web-dev-qa-db-fra.com

Différence entre Monad et Applicative à Haskell

Je viens de lire ce qui suit dans typeclassopedia sur la différence entre Monad et Applicative. Je peux comprendre qu'il n'y a pas de join dans Applicative. Mais la description suivante me semble vague et je n'ai pas pu comprendre ce que l'on entend exactement par "le résultat" d'un calcul/action monadique. Donc, si je mets une valeur dans Maybe, ce qui fait une monade, quel est le résultat de ce "calcul"?

Examinons de plus près le type de (>> =). L'intuition de base est qu'il combine deux calculs en un seul calcul plus grand. Le premier argument, m a, est le premier calcul. Cependant, ce serait ennuyeux si le deuxième argument n'était qu'un m b; alors il n'y aurait aucun moyen pour les calculs d'interagir les uns avec les autres (en fait, c'est exactement la situation avec Applicative). Ainsi, le deuxième argument de (>> =) a le type a -> m b: une fonction de ce type, étant donné le résultat du premier calcul, peut produire un deuxième calcul à exécuter. ... Intuitivement, c'est cette capacité à utiliser la sortie des calculs précédents pour décider quels calculs exécuter ensuite qui rend Monad plus puissant qu'Applicatif. La structure d'un calcul Applicatif est fixe, tandis que la structure d'un calcul Monade peut changer en fonction de résultats intermédiaires.

Existe-t-il un exemple concret illustrant "la capacité à utiliser la sortie des calculs précédents pour décider quels calculs exécuter ensuite", quel applicatif n'a pas?

49
tinlyx

Mon exemple préféré est le "Soit purement applicatif". Nous allons commencer par analyser l'instance de base Monad pour Soit

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

Cette instance intègre une notion de court-circuit très naturelle: nous procédons de gauche à droite et une fois qu'un calcul "échoue" dans le Left, alors tout le reste fait de même. Il y a aussi l'instance naturelle de Applicative que tout Monad a

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

ap n'est rien d'autre qu'un séquencement de gauche à droite avant un return:

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

Maintenant, le problème avec cette instance Either apparaît lorsque vous souhaitez collecter des messages d'erreur qui se produisent n'importe où dans un calcul et produire en quelque sorte un résumé des erreurs. Cela va à l'encontre des courts-circuits. Il va également à l'encontre du type de (>>=)

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

Si nous pensons à m a comme "le passé" et m b comme "l'avenir" puis (>>=) produit l'avenir à partir du passé tant qu'il peut exécuter le "stepper" (a -> m b). Ce "stepper" exige que la valeur de a existe vraiment à l'avenir ... et cela est impossible pour Either. Par conséquent (>>=)demande court-circuit.

Donc, à la place, nous implémenterons une instance Applicative qui ne peut pas avoir une Monad correspondante.

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

Maintenant, la mise en œuvre de (<*>) est la partie spéciale qui mérite une attention particulière. Il effectue un certain "court-circuit" dans ses premiers cas, mais fait quelque chose d'intéressant dans le quatrième.

  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!

Notez encore que si nous considérons l'argument de gauche comme "le passé" et l'argument de droite comme "le futur", alors (<*>) est spécial par rapport à (>>=) car il est permis "d'ouvrir" le futur et le passé en parallèle au lieu d'avoir nécessairement besoin des résultats du "passé" pour calculer "le futur".

Cela signifie, directement, que nous pouvons utiliser nos purement ApplicativeEither pour collecter les erreurs, en ignorant Rights si des Lefts existent dans la chaîne

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

Alors retournons cette intuition sur sa tête. Que ne pouvons-nous pas faire avec un Either purement applicatif? Eh bien, puisque son fonctionnement dépend de l'examen du futur avant de courir le passé, nous devons être en mesure de déterminer la structure du futur sans dépendre des valeurs du passé. En d'autres termes, nous ne pouvons pas écrire

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

qui satisfait les équations suivantes

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

alors que nous pouvons écrire 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

tel que

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

Cette impossibilité survient parce que ifA incarne exactement l'idée du calcul du résultat en fonction des valeurs intégrées dans les calculs d'argument.

62
J. Abrahamson

Just 1 décrit un "calcul", dont le "résultat" est 1. Nothing décrit un calcul qui ne produit aucun résultat.

La différence entre une Monade et un Applicatif est que dans la Monade il y a un choix. La principale distinction des Monades est la possibilité de choisir entre différents chemins dans le calcul (pas seulement de sortir tôt). En fonction d'une valeur produite par une étape précédente du calcul, le reste de la structure de calcul peut changer.

Voici ce que cela signifie. Dans la chaîne monadique

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

le if choisit le calcul à construire.

En cas de demande, en

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

toutes les fonctions fonctionnent "à l'intérieur" des calculs, il n'y a aucune chance de briser une chaîne. Chaque fonction transforme simplement une valeur qu'elle est alimentée. La "forme" de la structure de calcul est entièrement "à l'extérieur" du point de vue des fonctions.

Une fonction peut renvoyer une valeur spéciale pour indiquer l'échec, mais elle ne peut pas ignorer les étapes suivantes du calcul. Ils devront tous également traiter la valeur spéciale d'une manière spéciale. La forme du calcul ne peut pas être modifiée en fonction de la valeur reçue.

Avec les monades, les fonctions elles-mêmes construisent des calculs à leur choix.

38
Will Ness

Voici mon point de vue sur @J. Exemple d'Abrahamson expliquant pourquoi ifA ne peut pas utiliser la valeur à l'intérieur, par exemple (pure True). En substance, cela se résume toujours à l'absence de la fonction join de Monad dans Applicative, qui unifie les deux perspectives différentes données dans typeclassopedia pour expliquer la différence entre Monad et Applicative.

Donc, en utilisant @J. Exemple d'Abrahamson de Either purement applicatif:

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!

(qui a un effet de court-circuit similaire à la fonction EitherMonad) et à la fonction ifA

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

Et si nous essayons d'atteindre les équations mentionnées:

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

?

Eh bien, comme déjà indiqué, en fin de compte, le contenu de (pure True), Ne peut pas être utilisé par un calcul ultérieur. Mais techniquement parlant, ce n'est pas juste. Nous pouvons utiliser le contenu de (pure True) Car un Monad est aussi un Functor avec fmap. Nous pouvons faire:

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

Le problème vient du type de retour de ifA', Qui est f (f a). Dans Applicative, il n'y a aucun moyen de regrouper deux ApplicativeS imbriqués en un seul. Mais cette fonction d'effondrement est précisément ce que join dans Monad effectue. Donc,

ifA = join . ifA' 

satisfera les équations de ifA, si nous pouvons implémenter join de manière appropriée. Ce que Applicative manque ici est exactement la fonction join. En d'autres termes, nous pouvons en quelque sorte utiliser le résultat du résultat précédent dans Applicative. Mais le faire dans un cadre Applicative impliquera d'augmenter le type de la valeur de retour à une valeur applicative imbriquée, que nous n'avons aucun moyen de ramener à une valeur applicative à un seul niveau. Ce sera un problème grave car, par exemple, nous ne pouvons pas composer les fonctions en utilisant ApplicativeS de manière appropriée. L'utilisation de join résout le problème, mais l'introduction même de join fait passer le Applicative en Monad.

15
tinlyx

La clé de la différence peut être observée dans le type de ap vs le type de =<<.

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

Dans les deux cas, il y a m a, Mais seulement dans le second cas m a Peut décider si la fonction (a->m b) Est appliquée. À son tour, la fonction (a->m b) Peut "décider" si la fonction liée est ensuite appliquée - en produisant un tel m b Qui ne "contient" pas b (comme [], Nothing ou Left).

Dans Applicative, les fonctions "internes" m (a->b) ne peuvent pas prendre de telles "décisions" - elles produisent toujours une valeur de type 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.

Dans Applicative ce n'est pas possible, donc ne peut pas montrer d'exemple. Le plus proche est:

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
13
Sassa NF

Mais la description suivante me semble vague et je n'ai pas pu comprendre ce que l'on entend exactement par "le résultat" d'un calcul/action monadique.

Eh bien, ce flou est quelque peu délibéré, car ce que "le résultat" d'un calcul monadique est quelque chose qui dépend de chaque type. La meilleure réponse est un peu tautologique: le "résultat" (ou résultats, car il peut y en avoir plusieurs) est la ou les valeurs que la mise en œuvre de l'instance de (>>=) :: Monad m => m a -> (a -> m b) -> m b Appelle l'argument de fonction avec.

Donc, si je mets une valeur dans Maybe, ce qui fait une monade, quel est le résultat de ce "calcul"?

La monade Maybe ressemble à ceci:

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

La seule chose ici qui peut être qualifiée de "résultat" est le a dans la deuxième équation de >>=, Car c'est la seule chose qui soit "alimentée" au deuxième argument de >>=.

D'autres réponses ont été approfondies concernant la différence entre ifA et ifM, alors j'ai pensé mettre en évidence une autre différence significative: les applicatifs composent, les monades ne font pas ' t . Avec Monads, si vous voulez créer un Monad qui combine les effets de deux effets existants, vous devez réécrire l'un d'eux en tant que transformateur monade. En revanche, si vous avez deux Applicatives, vous pouvez facilement en créer un plus complexe, comme indiqué ci-dessous. (Le code est copypassé de 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)

Maintenant, si nous ajoutons le foncteur Constant/applicatif:

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)

... nous pouvons assembler le "applicatif Either" des autres réponses à partir de Lift et Constant:

type Error e a = Lift (Constant e) a
2
Luis Casillas

Je voudrais partager mon point de vue sur cette chose "iffy miffy", car je comprends que tout dans le contexte est appliqué, donc par exemple:

iffy :: Applicative f => f Bool -> f a -> f a -> f a
iffy fb ft fe = cond <$> fb <*> ft <*> fe   where
            cond b t e = if b then t else e

case 1>> iffy (Just True) (Just “True”) Nothing ->> Nothing

upps devrait être juste "vrai" ... mais

 case 2>> iffy (Just False) (Just “True”) (Just "False") ->> Just "False" 

(le "bon" choix est fait dans le contexte) Je m'explique de cette façon, juste avant la fin du calcul au cas où >> 1 on obtient quelque chose comme ça dans la "chaîne":

Just (Cond True "True") <*> something [something being "accidentaly" Nothing]

qui selon la définition de Applicative est évalué comme:

fmap (Cond True "True") something 

qui quand "quelque chose" est Rien devient un rien selon la contrainte de Functor (fmap sur Nothing ne donne rien). Et il n'est pas possible de définir un Functor avec "fmap f Nothing = something" en fin d'histoire.

0
user3680029