Les applicatifs composent, les monades ne le font pas

110

Les applicatifs composent, les monades non.

Que signifie la déclaration ci-dessus? Et quand l'un est-il préférable à l'autre?

manquant
la source
5
D'où tenez-vous cette déclaration? Il peut être utile de voir un certain contexte.
fuz
@FUZxxl: Je l'ai entendu à plusieurs reprises de la part de nombreuses personnes différentes, récemment de debasishg sur Twitter.
missingfaktor
3
@stephen tetley: Notez que beaucoup de ces Applicatives sont en fait une famille entière de Monads, à savoir un pour chaque "forme" de structure possible. ZipListn'est pas a Monad, mais les ZipLists d'une longueur fixe le sont. Readerest un cas spécial pratique (ou est-ce général?) où la taille de la "structure" est fixée comme cardinalité du type d'environnement.
CA McCann
3
@CAMcCann Tous ces applicatifs zippy (qu'ils tronquent ou remplissent) se limitent aux monades si vous fixez la forme d'une manière qui équivaut à une Readermonade jusqu'à l'isomorphisme. Une fois que vous avez fixé la forme d'un conteneur, il encode efficacement une fonction à partir de positions, comme un mémo trie. Peter Hancock appelle ces foncteurs "Naperian", car ils obéissent aux lois des logarithmes.
pigworker
4
@stephen tetley: D'autres exemples incluent l'applicatif monoïde constant (qui est une composition de monades mais pas une monade), et l'applicateur à retard unitaire (qui ferait mieux de ne pas admettre de jointure).
pigworker

Réponses:

115

Si nous comparons les types

(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m =>       m s -> (s -> m t) -> m t

nous avons une idée de ce qui sépare les deux concepts. Cela (s -> m t)dans le type de (>>=)montre qu'une valeur dans speut déterminer le comportement d'un calcul dans m t. Les monades permettent des interférences entre la valeur et les couches de calcul. L' (<*>)opérateur n'autorise pas de telles interférences: les calculs de fonction et d'argument ne dépendent pas de valeurs. Cela mord vraiment. Comparer

miffy :: Monad m => m Bool -> m x -> m x -> m x
miffy mb mt mf = do
  b <- mb
  if b then mt else mf

qui utilise le résultat d'un certain effet pour décider entre deux calculs (par exemple le lancement de missiles et la signature d'un armistice), alors que

iffy :: Applicative a => a Bool -> a x -> a x -> a x
iffy ab at af = pure cond <*> ab <*> at <*> af where
  cond b t f = if b then t else f

qui utilise la valeur de abpour choisir entre les valeurs de deux calculs atet af, après avoir effectué les deux, peut-être à un effet tragique.

La version monadique repose essentiellement sur la puissance supplémentaire de (>>=)choisir un calcul à partir d'une valeur, et cela peut être important. Cependant, soutenir ce pouvoir rend les monades difficiles à composer. Si nous essayons de construire une double liaison

(>>>>==) :: (Monad m, Monad n) => m (n s) -> (s -> m (n t)) -> m (n t)
mns >>>>== f = mns >>-{-m-} \ ns -> let nmnt = ns >>= (return . f) in ???

nous arrivons jusqu'ici, mais maintenant nos couches sont toutes mélangées. Nous avons un n (m (n t)), donc nous devons nous débarrasser de l'extérieur n. Comme le dit Alexandre C, nous pouvons le faire si nous avons un

swap :: n (m t) -> m (n t)

permuter l' nintérieur et joinlui à l'autre n.

La `` double application '' plus faible est beaucoup plus facile à définir

(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs

car il n'y a pas d'interférence entre les couches.

En conséquence, il est bon de reconnaître quand vous avez vraiment besoin de la puissance supplémentaire de Monads et quand vous pouvez vous en sortir avec la structure de calcul rigide qui Applicativeprend en charge.

Notez, en passant, que bien que composer des monades soit difficile, cela peut être plus que ce dont vous avez besoin. Le type m (n v)indique le calcul avec m-effects, puis le calcul avec n-effects à une v-value, où les m-effects se terminent avant le ndébut des -effects (d'où la nécessité de swap). Si vous voulez juste entrelacer m-effets avec n-effects, alors la composition est peut-être trop demander!

ouvrier porcin
la source
3
Pour l'exemple douteux, vous dites qu'il "utilise la valeur de ab pour choisir entre les valeurs de deux calculs at et af, après avoir effectué les deux, peut-être avec un effet tragique". La nature paresseuse de Haskell ne vous protège-t-elle pas contre cela? Si j'ai list = (\ btf -> if b puis t else f): [] puis exécutez l'instruction: list <*> pure True <*> pure "hello" <*> pure (error "bad"). ... J'obtiens "bonjour" et l'erreur ne se produit jamais. Ce code n'est pas aussi sûr ou contrôlé qu'une monade, mais l'article semble suggérer que les applicatifs provoquent une évaluation stricte. Dans l'ensemble, c'est un excellent article! Merci!
shj
7
Vous obtenez toujours les effets des deux, mais pure (erreur "mauvais") n'en a pas. Si, d'un autre côté, vous essayez iffy (pur True) (pur "hello") (erreur "bad"), vous obtenez une erreur que miffy évite. De plus, si vous essayez quelque chose comme iffy (pure True) (pure 0) [1,2], vous obtiendrez [0,0] au lieu de [0]. Les applicatifs ont une sorte de rigueur à leur sujet, en ce sens qu'ils construisent des séquences fixes de calculs, mais les valeurs résultant de ces calculs sont toujours combinées paresseusement, comme vous l'observez.
pigworker
Est-il vrai que pour toutes les monades met que nvous pouvez toujours écrire un transformateur de monade mtet fonctionner en n (m t)utilisant mt n t? Donc on peut toujours composer des monades, c'est juste plus compliqué, en utilisant des transformateurs?
ron
4
De tels transformateurs existent souvent, mais pour autant que je sache, il n'y a pas de moyen canonique de les générer. Il y a souvent un véritable choix sur la façon de résoudre les effets entrelacés des différentes monades, l'exemple classique étant les exceptions et l'état. Une exception doit-elle annuler les changements d'état ou non? Les deux choix ont leur place. Cela dit, il existe une chose de "monade libre" qui exprime un "entrelacement arbitraire". data Free f x = Ret x | Do (f (Free f x)), alors data (:+:) f g x = Inl (f x) | Tnr (g x), et réfléchissez Free (m :+: n). Cela retarde le choix de la manière d'exécuter les entrelacements.
pigworker
@pigworker Concernant le débat paresseux / strict. Je pense qu'avec vous ne pouvez pas applicatifs contrôler l'effet de l' intérieur du calcul, mais l' effet couche peut très bien décider de ne pas évaluer les valeurs plus tard. Pour les analyseurs (applicatifs), cela signifie que si l'analyseur échoue tôt, les analyseurs suivants ne sont pas évalués / appliqués à l'entrée. Car Maybecela signifie qu'un début Nothingsupprimera l'évaluation du aplus tard / suivant Just a. Est-ce correct?
ziggystar
75

Les applicatifs composent, les monades non.

Monades font Compose, mais le résultat peut - être pas une monade. En revanche, la composition de deux applicatifs est forcément un applicatif. Je soupçonne que l'intention de la déclaration originale était que «l'applicitation compose, tandis que la monadisme ne le fait pas». Reformulé, « Applicativeest fermé sous la composition, et Monadn'est pas».

Conal
la source
24
De plus, deux applicatifs quelconques se composent de manière complètement mécanique, alors que la monade formée par la composition de deux monades est spécifique à cette composition.
Apocalisp
12
De plus, les monades composent autrement, le produit de deux monades est une monade, ce ne sont que les coproduits qui ont besoin d'une sorte de loi distributive.
Edward KMETT
Avec, @Apocalisp, commentaire inclus, c'est la réponse la meilleure et la plus concise.
Paul Draper
39

Si vous avez des applicatifs A1et A2, alors le type data A3 a = A3 (A1 (A2 a))est également applicatif (vous pouvez écrire une telle instance de manière générique).

Par contre, si vous avez des monades M1et que M2le type data M3 a = M3 (M1 (M2 a))n'est pas forcément une monade (il n'y a pas d'implémentation générique sensée pour >>=ou joinpour la composition).

Un exemple peut être le type [Int -> a](ici nous composons un constructeur de type []avec (->) Int, qui sont tous deux des monades). Vous pouvez facilement écrire

app :: [Int -> (a -> b)] -> [Int -> a] -> [Int -> b]
app f x = (<*>) <$> f <*> x

Et cela se généralise à tout applicatif:

app :: (Applicative f, Applicative f1) => f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)

Mais il n'y a pas de définition sensée de

join :: [Int -> [Int -> a]] -> [Int -> a]

Si vous n'êtes pas convaincu de cela, considérez cette expression:

join [\x -> replicate x (const ())]

La longueur de la liste retournée doit être définie dans la pierre avant qu'un entier ne soit fourni, mais la longueur correcte de celle-ci dépend de l'entier fourni. Ainsi, aucune joinfonction correcte ne peut exister pour ce type.

Rotsor
la source
1
... alors évitez les monades quand une fonction fera l'affaire?
andrew cooke
2
@andrew, si vous vouliez dire foncteur, alors oui, les foncteurs sont plus simples et devraient être utilisés quand ils sont suffisants. Notez que ce n'est pas toujours le cas. Par exemple, IOsans un Monadserait très difficile à programmer. :)
Rotsor
17

Malheureusement, notre véritable objectif, la composition de monades, est un peu plus difficile. .. En fait, nous pouvons en fait prouver que, dans un certain sens, il n'y a aucun moyen de construire une fonction de jointure avec le type ci-dessus en utilisant uniquement les opérations des deux monades (voir l'annexe pour un aperçu de la preuve). Il s'ensuit que la seule façon dont nous pourrions espérer former une composition est s'il existe des constructions supplémentaires reliant les deux composants.

Composer des monades, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf

Landei
la source
4
Tl; dr pour les lecteurs impatients: vous pouvez composer des monades si (f?) Vous pouvez fournir une transformation naturelleswap : N M a -> M N a
Alexandre C.
@Alexandre C .: Juste "si", je suppose. Tous les transformateurs monades ne sont pas décrits par composition directe de foncteurs. Par exemple, ContT r m an'est ni m (Cont r a)ni Cont r (m a), et StateT s m aest approximativement Reader s (m (Writer s a)).
CA McCann
@CA McCann: Je n'arrive pas à passer de (M monade, N monade, MN monade, NM monade) à (il existe un swap: MN -> NM naturel). Alors restons fidèles au "si" pour l'instant (peut-être que la réponse est dans le journal, je dois avouer que je l'ai recherchée rapidement)
Alexandre C.
1
@Alexandre C .: Préciser simplement que les compositions sont des monades peut ne pas suffire de toute façon - vous avez également besoin d'un moyen de relier les deux parties au tout. L'existence de swapimplique que la composition laisse les deux "coopérer" d'une manière ou d'une autre. Notez également qu'il sequences'agit d'un cas particulier de "swap" pour certaines monades. Ainsi est flip, en fait.
CA McCann
7
Pour l'écrire, swap :: N (M x) -> M (N x)il me semble que vous pouvez utiliser returns(convenablement fmapped) pour insérer un Mà l'avant et un Nà l'arrière, en partant de N (M x) -> M (N (M (N x))), puis utiliser le joindu composite pour obtenir votre M (N x).
pigworker
7

La solution de loi distributive l: MN -> NM suffit

pour garantir la monadicité de NM. Pour voir cela, vous avez besoin d'une unité et d'un mult. Je vais me concentrer sur le mult (l'unité est unit_N unitM)

NMNM - l -> NNMM - mult_N mult_M -> NM

Cela ne garantit pas que MN est une monade.

L'observation cruciale entre cependant en jeu lorsque vous avez des solutions de droit de la distribution

l1 : ML -> LM
l2 : NL -> LN
l3 : NM -> MN

ainsi, LM, LN et MN sont des monades. La question se pose de savoir si LMN est une monade (soit par

(MN) L -> L (MN) ou par N (LM) -> (LM) N

Nous avons suffisamment de structure pour créer ces cartes. Cependant, comme l' observe Eugenia Cheng , nous avons besoin d'une condition hexagonale (qui équivaut à une présentation de l'équation de Yang-Baxter) pour garantir la monadicité de l'une ou l'autre construction. En fait, avec la condition hexagonale, les deux monades différentes coïncident.

user278559
la source
9
Ceci est probablement une excellente réponse, mais il a whoosh chemin sur ma tête.
Dan Burton
1
En effet, en utilisant le terme Applicative et tag haskell, il s'agit d'une question sur haskell mais avec une réponse dans une notation différente.
codehot