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?

33
demandé sur Will Ness 2013-07-01 20:26:15

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 .

55
répondu Carl 2013-07-02 08:58:30

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é:

24
répondu Petr Pudlák 2017-05-23 12:10:41

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!
21
répondu Antal Spector-Zabusky 2017-05-23 12:26:35

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"
10
répondu J. Abrahamson 2013-07-03 14:00:56

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.

7
répondu MathematicalOrchid 2013-07-02 07:45:21

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 à <*>

5
répondu Ankur 2013-07-02 05:03:30

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] .

5
répondu Landei 2013-07-02 07:37:36