Je pense que la façon la plus simple de comprendre la monade Cont
est de comprendre comment utiliser son constructeur. Je vais supposer que la définition suivante pour l'instant, bien que les réalités du paquet transformers
sont légèrement différentes:
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
Cela donne:
Cont :: ((a -> r) -> r) -> Cont r a
afin de construire une valeur de type Cont r a
, nous besoin de donner une fonction à Cont
:
value = Cont $ \k -> ...
maintenant, k
lui-même est de type a -> r
, et le corps du lambda doit avoir le type r
. Une chose évidente à faire serait d'appliquer k
à une valeur de type a
, et d'obtenir une valeur de type r
. Nous pouvons le faire, oui, mais ce n'est vraiment qu'une des nombreuses choses que nous pouvons faire. Rappelez-vous que value
n'a pas besoin d'être polymorphe dans r
, il pourrait être de type Cont String Integer
ou autre chose de béton. Alors:
- Nous pourrions appliquer
k
à plusieurs valeurs de type a
, et combiner les résultats en quelque sorte.
- Nous pourrions appliquer
k
à une valeur de type a
, observer le résultat, puis décider d'appliquer k
à autre chose en fonction de cela.
- Nous pourrions ignorer complètement
k
et juste produire une valeur de type r
nous-mêmes.
Mais qu'est-ce que tout cela signifie? Que devient k
? Eh bien, dans un do-bloc, nous pourrions avoir quelque chose qui ressemble à ceci:
flip runCont id $ do
v <- thing1
thing2 v
x <- Cont $ \k -> ...
thing3 x
thing4
Voici la partie amusante: nous pouvons, dans nos esprits et un peu informelle, diviser le do-bloc en deux à l'apparition du Cont
constructeur, et penser au reste du calcul entier après comme une valeur en soi. Mais attendez, ce qu'elle est dépend de ce que x
est, il est donc vraiment une fonction d'une valeur x
de type a
à une valeur de résultat:
restOfTheComputation x = do
thing3 x
thing4
En fait, ce restOfTheComputation
est grosso modo ce k
finit par être. En d'autres termes, vous appelez k
avec une valeur qui devient le résultat x
de votre calcul Cont
, le reste du calcul s'exécute, puis le r
produit son chemin de retour dans votre lambda à la suite de l'appel à k
. Alors:
- Si vous avez appelé
k
plusieurs fois, le reste du calcul va s'exécuter plusieurs fois, et les résultats peuvent être combinés comme vous le souhaitez.
- Si vous n'avez pas appelé
k
, le reste du calcul complet sera ignoré et l'appel runCont
qui vous renverra vous donnera la valeur r
que vous avez réussi à synthétiser. Autrement dit, à moins qu'une autre partie du calcul appelle vous de leurk
et tripoter le résultat ...
Si vous êtes toujours avec moi à ce stade, il devrait être facile voir cela pourrait être assez puissant. Pour faire un peu le point, implémentons quelques classes de types standard.
instance Functor (Cont r) where
fmap f (Cont c) = Cont $ \k -> ...
On nous donne une valeur Cont
avec un résultat de liaison x
de type a
, et une fonction f :: a -> b
, et nous voulons faire une valeur Cont
avec un résultat de liaison f x
de type b
. Eh bien, pour définir le résultat de liaison, il suffit d'appeler k
...
fmap f (Cont c) = Cont $ \k -> k (f ...
Attendez, où pouvons-nous x
de? Eh bien, cela va impliquer c
, que nous n'avons pas encore utilisé. Rappelez-vous comment fonctionne c
: il reçoit une fonction, puis appelle cette fonction avec son résultat de liaison. Nous voulons appeler notre fonction avec f
appliquée à ce résultat de liaison. Ainsi:
fmap f (Cont c) = Cont $ \k -> c (\x -> k (f x))
Tada!Ensuite, Applicative
:
instance Applicative (Cont r) where
pure x = Cont $ \k -> ...
Celui-ci est simple. Nous voulons que le résultat de liaison soit le x
obtenu.
pure x = Cont $ \k -> k x
Maintenant, <*>
:
Cont cf <*> Cont cx = Cont $ \k -> ...
C'est un peu plus délicat, mais utilise essentiellement les mêmes idées que dans fmap: d'abord obtenir la fonction de la première Cont
, en faisant un lambda pour lui appeler :
Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> ...
obtenez alors la valeur x
de la seconde, et de faire fn x
le résultat de liaison:
Cont cf <*> Cont cx = Cont $ \k -> cf (\fn -> cx (\x -> k (fn x)))
Monad
est la même, bien que nécessite runCont
ou d'un cas ou laisser déballer le newtype. Cette réponse est déjà assez longue, donc je ne vais pas entrer dans ContT
(en bref: c'est exactement la même chose que Cont
! La seule différence est dans le type du constructeur de type, les implémentations de tout sont identiques) ou callCC
(un combinateur utile qui fournit un moyen pratique d'ignorer k
, en implémentant la sortie anticipée d'un sous-bloc).
Pour une application simple et plausible, essayez l'article de blog de Edward Z. Yang implémentant labelled break and continue in for loops.
Connaissez-vous CPS? Sinon, vous devriez chercher des tutoriels sur ce sujet (je n'en connais pas moi-même), car cela rendrait Cont beaucoup plus facile. –