Quels sont les avantages d'Monade nous donnent sur un Applicatif?
j'ai lu cet article , mais je n'ai pas compris la dernière section.
l'auteur dit que Monad nous donne la sensibilité de contexte, mais il est possible d'obtenir le même résultat en utilisant seulement une instance Applicative:
let maybeAge = (futureYear birthYear -> if futureYear < birthYear
then yearDiff birthYear futureYear
else yearDiff futureYear birthYear) <$> (readMay futureYearString) <*> (readMay birthYearString)
c'est encore plus laid sans do-syntaxe, mais en plus, Je ne vois pas pourquoi nous avons besoin de Monad. Quelqu'un peut-il éclaircir ce point pour moi?
7 réponses
voici quelques fonctions qui utilisent l'interface Monad
.
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= \z -> if z then x else y
whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)
vous ne pouvez pas les implémenter avec l'interface Applicative
. Mais au nom de l'illumination, essayons de voir où les choses vont mal. Comment sur..
import Control.Applicative
ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (\c' x' y' -> if c' then x' else y') <$> c <*> x <*> y
semble bon! Il a le bon type, il doit être la même chose! Disons juste assurez-vous..
*Main> ifM (Just True) (Just 1) (Just 2)
Just 1
*Main> ifM (Just True) (Just 1) (Nothing)
Just 1
*Main> ifA (Just True) (Just 1) (Just 2)
Just 1
*Main> ifA (Just True) (Just 1) (Nothing)
Nothing
et voici votre premier indice à la différence. Vous ne pouvez pas écrire une fonction en utilisant seulement l'interface Applicative
qui reproduit ifM
.
si vous divisez cela en réflexion sur les valeurs de la forme f a
comme étant sur les" effets "et les" résultats " (les deux sont des termes approximatifs très flous qui sont les meilleurs termes disponibles, mais pas très bons), vous pouvez améliorer votre compréhension ici. Dans le cas de valeurs de type Maybe a
, l ' "effet" est le succès ou l'échec, comme un calcul. Le "résultat" est une valeur de type a
qui pourraient être présents lorsque le calcul est terminé. (La signification de ces termes dépend fortement du type concret, alors ne pensez pas que ce soit une description valide de quoi que ce soit d'autre que Maybe
comme un type.)
étant donné ce réglage, nous pouvons regarder la différence un peu plus en profondeur. L'interface Applicative
permet au débit de commande" résultat "d'être dynamique, mais il exige que le débit de commande" effet " soit statique. Si votre expression implique 3 calculs qui peuvent tomber en panne, la panne de l'un d'eux provoque l'échec de l'ensemble du calcul. L'interface Monad
est plus flexible. Il permet au flux de contrôle de l '" effet "de dépendre des valeurs du" résultat". ifM
choisit les" effets "de l'argument à inclure dans ses propres" effets " basés sur son premier argument. C'est l'énorme différence fondamentale entre ifA
et ifM
.
il y a quelque chose d'encore plus grave on continue avec whileM
. Essayons de faire whileA
et voyons ce qui se passe.
whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)
bien.. Ce qui se passe est une erreur de compilation. (<*>)
n'a pas le bon type. whileA p step
a le type a -> f a
et step x
a le type f a
. (<*>)
n'est pas la bonne forme pour ajustement ensemble. Pour que cela fonctionne, le type de fonction devra être f (a -> a)
.
Vous pouvez essayer beaucoup plus de choses - mais vous finirez par découvrir que whileA
n'a pas d'implémentation qui fonctionne même de la façon dont whileM
le fait. Je veux dire, vous pouvez implémenter le type, mais il n'y a aucun moyen de le faire à la fois boucle et se terminer.
Faire fonctionner exige soit join
ou (>>=)
. (Eh bien, ou l'un des nombreux équivalents de l'un d'eux) et ceux les choses supplémentaires que vous obtenez de l'interface Monad
.
avec les monades, les effets ultérieurs peuvent dépendre des valeurs précédentes. Par exemple, vous pouvez avoir:
main = do
b <- readLn :: IO Bool
if b
then fireMissiles
else return ()
vous ne pouvez pas faire cela avec Applicative
s - la valeur de résultat d'un calcul effectfull ne peut pas déterminer quel effet suivra.
quelque peu apparenté:
comme L'a dit Stephen Tetley dans un commentaire , cet exemple n'utilise pas réellement la sensibilité au contexte. Une façon de penser la sensibilité au contexte est qu'elle permet de choisir les actions à prendre en fonction des valeurs monadiques. Les calculs applicatifs doivent toujours avoir la même" forme", dans un certain sens, indépendamment des valeurs impliquées; les calculs monadiques n'ont pas besoin. Je pense personnellement que c'est plus facile à comprendre avec un exemple concret, alors regardons un. Voici deux versions d'un programme simple qui vous demande d'entrer un mot de passe, de vérifier que vous avez entré le bon, et d'imprimer une réponse selon si oui ou non vous l'avez fait.
import Control.Applicative
checkPasswordM :: IO ()
checkPasswordM = do putStrLn "What's the password?"
pass <- getLine
if pass == "swordfish"
then putStrLn "Correct. The secret answer is 42."
else putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
checkPasswordA :: IO ()
checkPasswordA = if' . (== "swordfish")
<$> (putStrLn "What's the password?" *> getLine)
<*> putStrLn "Correct. The secret answer is 42."
<*> putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
if' :: Bool -> a -> a -> a
if' True t _ = t
if' False _ f = f
chargeons ceci dans GHCi et vérifions ce qui se passe avec la version monadique:
*Main> checkPasswordM
What's the password?
swordfish
Correct. The secret answer is 42.
*Main> checkPasswordM
What's the password?
zvbxrpl
INTRUDER ALERT! INTRUDER ALERT!
jusqu'ici, tout va bien. Mais si nous utilisons la version applicative:
*Main> checkPasswordA
What's the password?
hunter2
Correct. The secret answer is 42.
INTRUDER ALERT! INTRUDER ALERT!
nous avons entré le mauvais mot de passe, secret! et une alerte intrus! C'est parce que <$>
et <*>
, ou l'équivalent liftAn
/ liftMn
, toujours exécuter les effets de tous leurs arguments. La version applicative se traduit, en do
notation, en
do pass <- putStrLn "What's the password?" *> getLine)
unit1 <- putStrLn "Correct. The secret answer is 42."
unit2 <- putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
pure $ if' (pass == "swordfish") unit1 unit2
et il devrait être clair pourquoi cela a le comportement erroné. En fait, chaque utilisation d'applicatifs functors est équivalent au code monadique de la forme
do val1 <- app1
val2 <- app2
...
valN <- appN
pure $ f val1 val2 ... valN
(où une partie du appI
peut être du formulaire pure xI
). Et de manière équivalente, tout code monadique sous cette forme peut être réécrit comme
f <$> app1 <*> app2 <*> ... <*> appN
ou équivalent en
liftAN f app1 app2 ... appN
pour penser à cela, considérez les méthodes de Applicative
:
pure :: a -> f a
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
et puis considérer ce que Monad
ajoute:
(=<<) :: (a -> m b) -> m a -> m b
join :: m (m a) -> m a
(rappelez-vous que vous n'en avez besoin que d'un seul.)
beaucoup de travail manuel, si vous y pensez, le seul moyen que nous pouvons mettre ensemble les fonctions applicatives est de construire des chaînes de la forme f <$> app1 <*> ... <*> appN
, et peut-être emboîter ces chaînes ( par exemple , f <$> (g <$> x <*> y) <*> z
). Cependant, (=<<)
(ou (>>=)
) nous permet de prendre une valeur et de produire différents monades calculs selon cette valeur, qui pourrait être construit à la volée. C'est ce que nous utilisons pour décider de calculer "imprimer le secret", ou calculer "imprimer une alerte de l'intrus", et pourquoi nous ne pouvons pas prendre cette décision avec les foncteurs applicatifs seul; aucun des types pour applicative fonctions vous permettent de consommer une simple valeur.
vous pouvez penser à join
en concert avec fmap
d'une manière similaire: comme je l'ai mentionné dans un commentaire , vous peut faire quelque chose comme
checkPasswordFn :: String -> IO ()
checkPasswordFn pass = if pass == "swordfish"
then putStrLn "Correct. The secret answer is 42."
else putStrLn "INTRUDER ALERT! INTRUDER ALERT!"
checkPasswordA' :: IO (IO ())
checkPasswordA' = checkPasswordFn <$> (putStrLn "What's the password?" *> getLine)
C'est ce qui se produit lorsque nous voulons choisir un calcul différent en fonction de la valeur, mais nous avons seulement la fonctionnalité applicative disponible. Nous pouvons choisir deux calculs différents pour revenir, mais ils sont enveloppés à l'intérieur de la couche extérieure du foncteur applicatif. Pour utiliser réellement le calcul que nous avons choisi, nous avons besoin de join
:
checkPasswordM' :: IO ()
checkPasswordM' = join checkPasswordA'
et cela fait même chose que la version précédente monadique (aussi longtemps que nous import Control.Monad
d'abord, pour obtenir join
):
*Main> checkPasswordM'
What's the password?
12345
INTRUDER ALERT! INTRUDER ALERT!
d'un autre côté, voici un exemple pratique du Applicative
/ Monad
diviser où Applicative
s ont un avantage: la manipulation des erreurs! Nous avons clairement une Monad
implémentation de Either
qui comporte des erreurs, mais elle se termine toujours tôt.
Left e1 >> Left e2 === Left e1
vous pouvez penser à cela comme un effet de mélange des valeurs et des contextes. Depuis (>>=)
va essayer de passer le résultat de la Either e a
la valeur d'une fonction a -> Either e b
, doit échouer immédiatement si l'entrée Either
est Left
.
Applicative
s ne passent leurs valeurs au calcul pur final qu'après avoir exécuté tous les effets. Cela signifie qu'ils peuvent retarder les valeurs plus longtemps et on peut écrire cela.
data AllErrors e a = Error e | Pure a deriving (Functor)
instance Monoid e => Applicative (AllErrors e) where
pure = Pure
(Pure f) <*> (Pure x) = Pure (f x)
(Error e) <*> (Pure _) = Error e
(Pure _) <*> (Error e) = Error e
-- This is the non-Monadic case
(Error e1) <*> (Error e2) = Error (e1 <> e2)
il est impossible d'écrire une instance Monad
pour AllErrors
telle que ap
correspond à (<*>)
parce que (<*>)
profite de l'exécution des deux contextes avant d'utiliser n'importe quelles valeurs afin d'obtenir les deux erreurs et (<>)
eux ensemble. Monad
ic (>>=)
et (join)
peut uniquement accéder à des contextes imbriqués, avec leurs valeurs. C'est pourquoi l'instance Either
's Applicative
est biaisée à gauche, de sorte qu'elle peut aussi avoir une instance Monad
harmonieuse.
> Left "a" <*> Left "b"
Left 'a'
> Error "a" <*> Error "b"
Error "ab"
avec applicatif, la séquence des actions efficaces à effectuer est fixée lors de la compilation. Avec Monad, il peut être modifié à l'exécution en fonction des résultats des effets.
par exemple, avec un analyseur applicatif, la séquence des actions d'analyse est fixe à tout moment. Cela signifie que vous pouvez éventuellement réaliser "optimisations". D'un autre côté, je peux écrire un analyseur monadique qui analyse une description grammaticale de la BNF, construit dynamiquement un analyseur pour cette grammaire, puis exécute cet analyseur sur le reste de l'entrée. Chaque fois que vous lancez cet analyseur, il construit potentiellement un analyseur flambant neuf pour analyser la seconde partie de l'entrée. Applicative n'a aucun espoir de faire une telle chose - et il n'y a aucune chance d'effectuer des optimisations de compilation sur un analyseur qui n'existe pas encore...
comme vous pouvez le voir, parfois la "limitation" de L'applicatif est en fait bénéfique - et parfois le pouvoir supplémentaire offert par Monad est nécessaire pour faire le travail. C'est pourquoi nous avons à la fois.
si vous essayez de convertir la signature de type de Monad bind
et applicatif <*>
en langage naturel, vous trouverez que:
bind
: I vous donnera la valeur contenue et vous me retournera une nouvelle valeur emballée
<*>
: vous me donnez une fonction packaged qui accepte une valeur contenue et retourner une valeur et I l'utilisera pour créer une nouvelle valeur packagée basée sur mes règles.
maintenant, comme vous pouvez le voir à partir de la description ci-dessus, bind
vous donne plus de contrôle par rapport à <*>
si vous travaillez avec des applicatifs, la" forme "du résultat est déjà déterminée par la" forme "de l'entrée, par exemple si vous appelez [f,g,h] <*> [a,b,c,d,e]
, votre résultat sera une liste de 15 éléments, quelles que soient les valeurs des variables. Vous n'avez pas cette garantie/limitation avec les monades. Considérez [x,y,z] >>= join replicate
: pour [0,0,0]
vous obtiendrez le résultat []
, pour [1,2,3]
le résultat [1,2,2,3,3,3]
.