web-dev-qa-db-fra.com

Monades avec Join () au lieu de Bind ()

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

fonction de jointure Monad

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

64
MathematicalOrchid

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:

  • si vous avez déjà une valeur, alors vous avez une stratégie pour produire une valeur (return :: v -> m v) consistant rien d'autre que la production de la valeur que vous avez;
  • si vous avez une fonction qui transforme une sorte de valeur en une autre, vous pouvez la déplacer vers des stratégies (fmap :: (v -> u) -> m v -> m u) simplement en attendant que la stratégie fournisse sa valeur, puis en la transformant;
  • si vous avez une stratégie pour produire une stratégie pour produire une valeur, alors vous pouvez construire une stratégie pour produire une valeur (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.

tree of trees

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 joins pour faire une stratégie produisant un Int.

enter image description here

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!

93
pigworker
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
26
Daniel Wagner

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 ...: - /

14
MathematicalOrchid

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.

12
solrize

Appel de fmap (f :: a -> m b) (x ::ma) 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 <$>)

Kleisli composition

11
Will Ness

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.

3
rdm

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

Monad explained in one picture.

1
Dagang