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?
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
où 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 Applicative
Either
pour collecter les erreurs, en ignorant Right
s si des Left
s 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.
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.
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 Either
Monad
) 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 Applicative
S 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 Applicative
S 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
.
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
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 Monad
s, 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
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.