Les monades sont généralement expliquées tour à tour par return
et bind
. Cependant, je suppose que vous pouvez également implémenter bind
en termes de join
(et fmap
?)
Dans les langages de programmation dépourvus de fonctions de première classe, bind
est extrêmement difficile à utiliser. join
, en revanche, semble assez facile.
Cependant, je ne suis pas sûr de comprendre comment fonctionne join
. De toute évidence, il a le type [Haskell]
join :: Monade m => m (m x) -> m x
Pour la liste monad, c'est trivialement et évidemment concat
. Mais pour une monade générale, que fait, opérationnellement, cette méthode? Je vois ce que cela fait aux signatures de type, mais j'essaie de comprendre comment j'écrirais quelque chose comme ça, par exemple, Java ou similaire.
(En fait, c'est facile: je ne le ferais pas. Parce que les génériques sont cassés. ;-) Mais en principe, la question reste posée ...)
Oups. Il semble que cela a déjà été demandé:
Quelqu'un pourrait-il esquisser quelques implémentations de monades courantes en utilisant return
, fmap
et join
? (C'est-à-dire, sans mentionner >>=
du tout.) Je pense que cela pourrait l'aider à s'enfoncer dans mon cerveau stupide ...
Sans sonder les profondeurs de la métaphore, puis-je suggérer de lire une monade typique m
comme "stratégie pour produire un", donc le type m value
Est une stratégie de première classe pour produire une valeur. Différentes notions de calcul ou d'interaction externe nécessitent différents types de stratégie, mais la notion générale nécessite une structure régulière pour avoir du sens:
return :: v -> m v
) consistant rien d'autre que la production de la valeur que vous avez;fmap :: (v -> u) -> m v -> m u
) simplement en attendant que la stratégie fournisse sa valeur, puis en la transformant;join :: m (m v) -> m v
) qui suit la stratégie externe jusqu'à ce qu'elle produise la stratégie interne, puis suit cette stratégie interne tout le chemin vers une valeur.Prenons un exemple: les arbres binaires étiquetés par des feuilles ...
data Tree v = Leaf v | Node (Tree v) (Tree v)
... représentent des stratégies pour produire des trucs en lançant une pièce. Si la stratégie est Leaf v
, Il y a votre v
; si la stratégie est Node h t
, vous lancez une pièce et continuez par la stratégie h
si la pièce montre "têtes", t
si c'est "queues".
instance Monad Tree where
return = Leaf
Une stratégie de production de stratégie est un arbre avec des feuilles étiquetées: à la place de chacune de ces feuilles, nous pouvons simplement greffer l'arbre qui l'étiquette ...
join (Leaf tree) = tree
join (Node h t) = Node (join h) (join t)
... et bien sûr nous avons fmap
qui ne fait que réétiqueter les feuilles.
instance Functor Tree where
fmap f (Leaf x) = Leaf (f x)
fmap f (Node h t) = Node (fmap f h) (fmap f t)
Voici une stratégie pour produire une stratégie pour produire un Int
.
Lancer une pièce: s'il s'agit de "têtes", lancer une autre pièce pour choisir entre deux stratégies (produire, respectivement, "lancer une pièce pour produire 0 ou produire 1" ou "produire 2"); s'il s'agit de "queues", en produire un troisième ("lancer une pièce pour en produire 3 ou lancer une pièce pour 4 ou 5").
C'est clairement join
s pour faire une stratégie produisant un Int
.
Ce que nous utilisons, c'est le fait qu'une "stratégie pour produire une valeur" peut elle-même être considérée comme une valeur. À Haskell, l'incorporation de stratégies en tant que valeurs est silencieuse, mais en anglais, j'utilise des guillemets pour distinguer l'utilisation d'une stratégie de celle d'en parler. L'opérateur join
exprime la stratégie "produire en quelque sorte puis suivre une stratégie", ou "si vous êtes dit une stratégie, vous pouvez alors tiliser la").
(Meta. Je ne sais pas si cette approche "stratégique" est une manière générique appropriée de penser aux monades et à la distinction valeur/calcul, ou si c'est juste une autre métaphore minable. Je trouve que les types arborescents étiquetés comme des feuilles sont utiles source d'intuition, ce qui n'est peut-être pas une surprise car ce sont les free monades, avec juste assez de structure pour être des monades, mais pas plus.)
PS Le type de "bind"
(>>=) :: m v -> (v -> m w) -> m w
dit "si vous avez une stratégie pour produire un v
, et pour chaque stratégie de suivi va pour produire un w
, alors vous avez une stratégie pour produire un w
" . Comment pouvons-nous saisir cela en termes de join
?
mv >>= v2mw = join (fmap v2mw mv)
Nous pouvons renommer notre stratégie de production de v
- par v2mw
, Produisant à la place de chaque valeur de v
la stratégie de production de w
- qui en découle - prête à join
!
join = concat -- []
join f = \x -> f x x -- (e ->)
join f = \s -> let (s', f') = f s in f' s' -- State
join (Just (Just a)) = Just a; join _ = Nothing -- Maybe
join (Identity (Identity a)) = Identity a -- Identity
join (Right (Right a)) = Right a; join (Right (Left e)) = Left e;
join (Left e) = Left e -- Either
join (m, (m', a)) = (m `mappend` m', a) -- Writer
join f = \k -> f (\f' -> f' k) -- Cont
OK, ce n'est donc pas vraiment une bonne forme pour répondre à votre propre question, mais je vais noter ma pensée au cas où cela éclairerait quelqu'un d'autre. (J'en doute...)
Si une monade peut être considérée comme un "conteneur", alors return
et join
ont une sémantique assez évidente. return
génère un conteneur à 1 élément et join
transforme un conteneur de conteneurs en un seul conteneur. Rien de difficile à ce sujet.
Concentrons-nous donc sur les monades qui sont plus naturellement considérées comme des "actions". Dans ce cas, m x
Est une sorte d'action qui donne une valeur de type x
lorsque vous l'exécutez. return x
Ne fait rien de spécial, puis renvoie x
. fmap f
Exécute une action qui génère un x
, et construit une action qui calcule x
, puis lui applique f
et renvoie le résultat. Jusqu'ici tout va bien.
Il est assez évident que si f
génère lui-même une action, alors vous vous retrouvez avec m (m x)
. Autrement dit, une action qui calcule une autre action. D'une certaine manière, il est peut-être encore plus simple de vous faire une idée que la fonction >>=
Qui prend une action et une "fonction qui produit une action" et ainsi de suite.
Donc, logiquement parlant, il semble que join
exécute la première action, exécute l'action qu'il produit, puis l'exécute. (Ou plutôt, join
retournerait une action qui fait ce que je viens de décrire, si vous voulez diviser les cheveux.)
Cela semble être l'idée centrale. Pour implémenter join
, vous souhaitez exécuter une action, qui vous donne ensuite une autre action, puis vous l'exécutez. (Quelle que soit la "course", cela signifie pour cette monade particulière.)
Compte tenu de ces informations, je peux essayer d'écrire quelques implémentations join
:
join Nothing = Nothing
join (Just mx) = mx
Si l'action extérieure est Nothing
, retourne Nothing
, sinon retourne l'action intérieure. Là encore, Maybe
est plus un conteneur qu'une action, alors essayons autre chose ...
newtype Reader s x = Reader (s -> x)
join (Reader f) = Reader (\ s -> let Reader g = f s in g s)
C'était ... indolore. Un Reader
n'est en fait qu'une fonction qui prend un état global et qui ne renvoie ensuite que son résultat. Donc, pour désempiler, vous appliquez l'état global à l'action externe, qui renvoie un nouveau Reader
. Vous appliquez ensuite également l'état à cette fonction intérieure.
D'une certaine manière, c'est peut-être plus facile que la manière habituelle:
Reader f >>= g = Reader (\ s -> let x = f s in g x)
Maintenant, laquelle est la fonction lecteur et laquelle est la fonction qui calcule le lecteur suivant ...?
Essayons maintenant la bonne vieille monade State
. Ici, chaque fonction prend un état initial en entrée mais renvoie également un nouvel état avec sa sortie.
data State s x = State (s -> (s, x))
join (State f) = State (\ s0 -> let (s1, State g) = f s0 in g s1)
Ce n'était pas trop dur. Il s'agit essentiellement d'une course suivie d'une course.
Je vais arrêter de taper maintenant. N'hésitez pas à signaler tous les pépins et fautes de frappe dans mes exemples ...: - /
J'ai trouvé de nombreuses explications sur les monades qui disent "vous n'avez rien à savoir sur la théorie des catégories, vraiment, pensez aux monades comme des burritos/combinaisons spatiales/quoi que ce soit".
Vraiment, l'article qui a démystifié les monades pour moi disait simplement quelles étaient les catégories, décrivait les monades (y compris rejoindre et lier) en termes de catégories, et ne se souciait d'aucune fausse métaphores:
Je pense que l'article est très lisible sans beaucoup de connaissances en mathématiques requises.
Appel de fmap (f :: a -> m b) (x ::
m
a)
Produit des valeurs (y ::
m
(m b))
C'est donc une chose très naturelle d'utiliser join
pour récupérer les valeurs (z :: m b)
.
La liaison est alors définie simplement comme bind ma f = join (fmap f ma)
, atteignant ainsi la compositionnalité de Kleisly des fonctions de la variété (:: a -> m b)
, Ce dont il s'agit vraiment:
ma `bind` (f >=> g) = (ma `bind` f) `bind` g -- bind = (>>=)
= (`bind` g) . (`bind` f) $ ma
= join . fmap g . join . fmap f $ ma
Et donc, avec flip bind = (=<<)
, nous avons
((g <=< f) =<<) = (g =<<) . (f =<<) = join . (g <$>) . join . (f <$>)
Demander ce qu'est une signature de type dans Haskell fait revient plutôt à demander ce qu'est une interface dans Java fait.
Il, dans un sens littéral, "ne le fait pas". (Bien que, bien sûr, vous ayez généralement une sorte d'objectif associé, c'est principalement dans votre esprit, et surtout pas dans la mise en œuvre.)
Dans les deux cas, vous déclarez des séquences légales de symboles dans la langue qui sera utilisée dans les définitions ultérieures.
Bien sûr, en Java, je suppose que vous pourriez dire qu'une interface correspond à une signature de type qui va être implémentée littéralement dans la machine virtuelle. Vous pouvez obtenir un certain polymorphisme de cette façon - vous pouvez définir un nom qui accepte une interface et vous pouvez fournir une définition différente pour le nom qui accepte une interface différente. Quelque chose de similaire se produit dans Haskell, où vous pouvez fournir une déclaration pour un nom qui accepte un type, puis une autre déclaration pour ce nom qui traite un type différent.
C'est Monad expliqué dans une image. Les 2 fonctions de la catégorie verte ne sont pas composables, lorsqu'elles sont mappées à la catégorie bleue (à proprement parler, elles sont une seule catégorie), elles deviennent composables. Monad consiste à transformer une fonction de type T -> Monad<U>
en fonction de Monad<T> -> Monad<U>
.