web-dev-qa-db-fra.com

Pourquoi avons-nous besoin de monades?

A mon humble avis, les réponses à la fameuse question "Qu'est-ce qu'une monade?" , en particulier les plus votées, essayez d'expliquer ce qu'est une monade sans expliquer clairement pourquoi les monades sont vraiment nécessaires . Peuvent-ils être expliqués comme la solution à un problème?

349
cibercitizen1

Pourquoi avons-nous besoin de monades?

  1. Nous voulons programmer niquement à l'aide de fonctions. ("programmation fonctionnelle (FP)" après tout).
  2. Ensuite, nous avons un premier gros problème. Ceci est un programme:

    f(x) = 2 * x

    g(x,y) = x / y

    Comment pouvons-nous dire ce qui doit être exécuté en premier? Comment pouvons-nous former une séquence ordonnée de fonctions (c'est-à-dire n programme) sans utiliser plus que des fonctions ?

    Solution: fonctions de composition. Si vous voulez d'abord g puis f, écrivez simplement f(g(x,y)). De cette façon, "le programme" est également une fonction: main = f(g(x,y)). OK mais ...

  3. Autres problèmes: certaines fonctions peuvent échouer (c'est-à-dire g(2,0), diviser par 0). Nous avons pas "d'exceptions" dans FP (une exception n'est pas une fonction). Comment le résolvons-nous?

    Laissons autorisons les fonctions à renvoyer deux types de choses: au lieu d'avoir g : Real,Real -> Real (fonction de deux réels en un réel), permettons à g : Real,Real -> Real | Nothing (fonction de deux réels en (réel ou rien)).

  4. Mais les fonctions devraient (pour être plus simples) ne renvoyer que ne chose.

    Solution: créons un nouveau type de données à renvoyer, un "type de boxe" qui renferme peut-être un réel ou ne sera tout simplement rien. Par conséquent, nous pouvons avoir g : Real,Real -> Maybe Real. OK mais ...

  5. Qu'advient-il maintenant de f(g(x,y))? f n'est pas prêt à consommer un Maybe Real. Et nous ne voulons pas changer toutes les fonctions auxquelles nous pourrions nous connecter avec g pour consommer un Maybe Real.

    Solution: avons une fonction spéciale pour "relier"/"composer"/"relier" des fonctions. De cette façon, nous pouvons, en coulisse, adapter la sortie d’une fonction à l’alimentation de la suivante.

    Dans notre cas: g >>= f (connectez/composez g à f). Nous voulons que >>= récupère la sortie de g, l'examine et, au cas où il s'agit de Nothing, n'appelle pas f et ne retourne Nothing; ou au contraire, extrayez la boîte Real et introduisez f avec elle. (Cet algorithme est juste l'implémentation de >>= pour le type Maybe). Notez également que >>= doit être écrit ne seule fois par "type de boxe" (boîte différente, algorithme d'adaptation différent).

  6. De nombreux autres problèmes peuvent être résolus en utilisant le même schéma: 1. Utilisez une "boîte" pour codifier/stocker différentes significations/valeurs et disposez de fonctions telles que g qui renvoient ces "valeurs encadrées". 2. Demandez à un composeur/à l'éditeur de liens g >>= f de faciliter la connexion de la sortie de g à l'entrée de f, de sorte que nous n'avons pas du tout besoin de modifier f.

  7. Les problèmes remarquables pouvant être résolus avec cette technique sont les suivants:

    • avoir un état global que toutes les fonctions de la séquence de fonctions ("le programme") peuvent partager: solution StateMonad.

    • Nous n'aimons pas les "fonctions impures": les fonctions qui produisent une sortie différente pour une entrée identique . Par conséquent, marquons ces fonctions en leur faisant renvoyer une valeur tagged/boxed: IO monad.

Bonheur total!

551
cibercitizen1

La réponse est, bien sûr, "Nous ne le faisons pas" . Comme avec toutes les abstractions, ce n'est pas nécessaire.

Haskell n'a pas besoin d'une abstraction de monade. Ce n'est pas nécessaire pour exécuter IO dans un langage pur. Le type IO prend soin de cela tout seul. La désinscription monadique existante des blocs do pourrait être remplacée par une désinsertion de bindIO, returnIO et failIO comme défini dans le module GHC.Base. (Ce n'est pas un module documenté sur le piratage, je vais donc devoir pointer vers sa source pour la documentation.) Donc non, l'abstraction de monade n'est pas nécessaire.

Donc, si ce n'est pas nécessaire, pourquoi existe-t-il? Parce qu'il a été constaté que de nombreux modèles de calcul forment des structures monadiques. L'abstraction d'une structure permet d'écrire un code qui fonctionne sur toutes les instances de cette structure. Pour être plus concis - réutilisation de code.

Dans les langages fonctionnels, l'outil le plus puissant trouvé pour la réutilisation de code a été la composition de fonctions. Le bon vieil opérateur (.) :: (b -> c) -> (a -> b) -> (a -> c) est extrêmement puissant. Il est facile d’écrire de petites fonctions et de les coller ensemble avec une surcharge syntaxique ou sémantique minime.

Mais il y a des cas où les types ne fonctionnent pas très bien. Que faites-vous quand vous avez foo :: (b -> Maybe c) et bar :: (a -> Maybe b)? foo . bar ne vérifie pas le texte car b et Maybe b ne sont pas du même type.

Mais ... c'est presque vrai. Vous voulez juste un peu de marge de manœuvre. Vous voulez pouvoir traiter Maybe b comme s'il s'agissait fondamentalement de b. C'est une mauvaise idée de les traiter comme si de rien n'était. C'est plus ou moins la même chose que les pointeurs nuls, que Tony Hoare avait pour nom célèbre l'erreur d'un milliard de dollars . Donc, si vous ne pouvez pas les traiter comme le même type, vous pouvez peut-être trouver un moyen d'étendre le mécanisme de composition fourni par (.).

Dans ce cas, il est important d'examiner réellement la théorie sous-jacente de (.). Heureusement, quelqu'un l'a déjà fait pour nous. Il s'avère que la combinaison de (.) et de id forme une construction mathématique appelée catégorie . Mais il existe d'autres moyens de former des catégories. Une catégorie Kleisli, par exemple, permet aux objets en cours de composition d’être un peu augmentés. Une catégorie de Kleisli pour Maybe serait composée de (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c) et id :: a -> Maybe a. C'est-à-dire que les objets de la catégorie augmentent le (->) d'un Maybe, de sorte que (a -> b) devient (a -> Maybe b).

Et tout à coup, nous avons étendu le pouvoir de la composition à des choses sur lesquelles l’opération traditionnelle (.) ne fonctionne pas. C'est une source de nouveau pouvoir d'abstraction. Les catégories de Kleisli fonctionnent avec plus de types que Maybe. Ils travaillent avec tous les types qui peuvent assembler une catégorie appropriée, obéissant aux lois de la catégorie.

  1. Identité de gauche: id . f = f
  2. Identité droite: f . id = f
  3. Associativité: f . (g . h) = (f . g) . h

Tant que vous pouvez prouver que votre type respecte ces trois lois, vous pouvez le transformer en une catégorie de Kleisli. Et quel est le problème à ce sujet? Eh bien, il s’avère que les monades sont exactement la même chose que les catégories de Kleisli. Monad 'return est identique à Kleisli id. Le (>>=) de Monad n'est pas identique à Kleisli (.), mais il s'avère très facile d'écrire chacun l'un l'autre. Et les lois de catégories sont les mêmes que les lois de monades, lorsque vous les traduisez à travers la différence entre (>>=) et (.).

Alors pourquoi passer par tout ce dérangement? Pourquoi avoir une abstraction Monad dans le langage? Comme je l'ai mentionné plus haut, cela permet la réutilisation du code. Il permet même la réutilisation de code selon deux dimensions différentes.

La première dimension de la réutilisation de code provient directement de la présence de l'abstraction. Vous pouvez écrire du code qui fonctionne sur toutes les instances de l'abstraction. Il y a tout le paquet monad-loops , constitué de boucles fonctionnant avec toutes les instances de Monad.

La deuxième dimension est indirecte, mais découle de l’existence de la composition. Lorsque la composition est facile, il est naturel d’écrire du code par petits morceaux réutilisables. De la même manière, l'opérateur (.) des fonctions encourage l'écriture de petites fonctions réutilisables.

Alors, pourquoi l'abstraction existe-t-elle? Parce que c'est un outil qui permet plus de composition dans le code, ce qui permet de créer du code réutilisable et encourage la création de code plus réutilisable. La réutilisation du code est l’un des plus grands défis de la programmation. L'abstraction de la monade existe parce qu'elle nous amène un peu vers ce saint Graal.

212
Carl

Benjamin Pierce a dit dans TAPL

Un système de types peut être considéré comme calculant une sorte d'approximation statique des comportements d'exécution des termes d'un programme.

C'est pourquoi un langage doté d'un système de types puissant est strictement plus expressif qu'un langage mal typé. Vous pouvez penser aux monades de la même manière.

Comme @Carl et sigfpe point, vous pouvez équiper un type de données de toutes les opérations souhaitées sans recourir à des monades, des classes de types ou à d’autres éléments abstraits. Cependant, les monades vous permettent non seulement d’écrire du code réutilisable, mais aussi d’abstraire tous les détails redondants.

Par exemple, supposons que nous voulions filtrer une liste. Le moyen le plus simple consiste à utiliser la fonction filter: filter (> 3) [1..10], qui est égale à [4,5,6,7,8,9,10].

Une version légèrement plus compliquée de filter, qui passe également un accumulateur de gauche à droite, est

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- Zip xs $ snd $ mapAccumL (swap .* f) a xs]

Pour obtenir tous les i, tels que i <= 10, sum [1..i] > 4, sum [1..i] < 25, nous pouvons écrire

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

qui est égal à [3,4,5,6].

Ou nous pouvons redéfinir la fonction nub, qui supprime les éléments en double d'une liste, en termes de filterAccum:

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4] est égal à [1,2,4,5,3,8,9]. Une liste est passée comme un accumulateur ici. Le code fonctionne, car il est possible de sortir de la liste monad, le calcul reste donc pur (notElem n'utilise pas >>= en réalité, mais il le pourrait). Cependant, il n’est pas possible de quitter le monade IO (c’est-à-dire que vous ne pouvez pas exécuter une action IO et renvoyer une valeur pure; la valeur sera toujours encapsulée dans le IO monade). Un autre exemple est les tableaux mutables: une fois que vous avez quitté le monad ST, où vit un tableau mutable, vous ne pouvez plus mettre à jour le tableau en temps constant. Nous avons donc besoin d’un filtrage monadique à partir du module Control.Monad:

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterM exécute une action monadique pour tous les éléments d'une liste, générant des éléments pour lesquels l'action monadique renvoie True.

Un exemple de filtrage avec un tableau:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

imprime [1,2,4,5,3,8,9] comme prévu.

Et une version avec le monade IO, qui demande quels éléments retourner:

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

Par exemple.

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

Et à titre d’illustration finale, filterAccum peut être défini en termes de filterM:

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

avec le StateT monad, qui est utilisé sous le capot, étant simplement un type de données ordinaire.

Cet exemple montre que les monades ne vous permettent pas seulement d’abstraire le contexte de calcul et d’écrire du code propre et réutilisable (en raison de la composabilité des monades, comme l'explique @Carl), mais également de traiter les types de données définis par l'utilisateur et les primitives intégrées de manière uniforme.

24
user3237465

Je ne pense pas que IO devrait être considéré comme une monade particulièrement remarquable, mais c'est certainement l'une des plus étonnantes pour les débutants, je vais donc l'utiliser pour mes explications.

Naïvement construire un système IO pour Haskell

Le système IO le plus simple imaginable pour un langage purement fonctionnel (et en fait celui avec lequel Haskell a commencé) est le suivant:

main₀ :: String -> String
main₀ _ = "Hello World"

Avec la paresse, cette simple signature suffit pour créer des programmes de terminaux interactifs - très limité, cependant. Le plus frustrant est que nous ne pouvons que produire du texte. Et si nous ajoutions des possibilités de sortie plus intéressantes?

data Output = TxtOutput String
            | Beep Frequency

main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

mignon, mais bien sûr une "sortie alternative" beaucoup plus réaliste serait écrire dans un fichier. Mais alors vous voudriez aussi un moyen de lire fichiers. Une chance?

Eh bien, lorsque nous prenons notre programme main₁ et simplement dirige un fichier vers le processus (en utilisant les installations du système d’exploitation), nous avons essentiellement implémenté la lecture de fichier. Si nous pouvions déclencher cette lecture de fichier à partir du langage Haskell ...

readFile :: Filepath -> (String -> [Output]) -> [Output]

Cela utiliserait un “programme interactif” String->[Output], lui donnerait une chaîne obtenue à partir d'un fichier et donnerait un programme non interactif exécutant simplement celui donné.

Il y a un problème ici: nous n'avons pas vraiment la notion de quand le fichier est lu. La liste [Output] donne bien un ordre de Nice à sorties, mais nous n'obtenons pas d'ordre pour le moment où entrées sera terminé.

Solution: créer des entrées-événements et des éléments de la liste des tâches à effectuer.

data IO₀ = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

Ok, maintenant vous pouvez remarquer un déséquilibre: vous pouvez lire un fichier et en faire une sortie dépendante de celui-ci, mais vous ne pouvez pas utiliser le contenu du fichier pour décider, par exemple, de lisez également un autre fichier. Solution évidente: indiquez également le résultat des entrées-événements de type IO, pas seulement Output. Cela inclut bien sûr une sortie texte simple, mais permet également de lire des fichiers supplémentaires, etc.

data IO₁ = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

Cela vous permettrait maintenant d'exprimer n'importe quelle opération de fichier que vous souhaitez peut-être dans un programme (mais peut-être pas avec de bonnes performances), mais c'est un peu trop compliqué:

  • main₃ donne un ensemble liste d'actions. Pourquoi n'utilisons-nous pas simplement la signature :: IO₁, qui a cela comme cas particulier?

  • Les listes ne donnent plus vraiment un aperçu fiable du déroulement du programme: la plupart des calculs ultérieurs ne seront "annoncés" qu'à la suite d'une opération d'entrée. Nous pourrions donc aussi bien abandonner la structure de la liste, et simplement contre un "puis faire" à chaque opération de sortie.

data IO₂ = TxtOut String IO₂
         | TxtIn (String -> IO₂)
         | Terminate

main₄ :: IO₂
main₄ = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

Pas mal!

Alors, qu'est-ce que tout cela a à voir avec les monades?

En pratique, vous ne voudriez pas utiliser des constructeurs simples pour définir tous vos programmes. Il faudrait un bon couple de ces constructeurs fondamentaux, mais pour la plupart des niveaux plus élevés, nous aimerions écrire une fonction avec une signature de Nice en haut. Il s'avère que la plupart de celles-ci semblent assez similaires: accepter une sorte de valeur significativement typée et générer une action IO comme résultat.

getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂

Il y a évidemment un modèle ici, et nous ferions mieux de l'écrire comme

type IO₃ a = (a -> IO₂) -> IO₂    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)

Cela commence à nous paraître familier, mais nous ne traitons toujours que de fonctions simples et dissimulées sous le capot, et c'est risqué: chaque "action-valeur" a la responsabilité de répercuter l'action résultante de toute fonction contenue (sinon le flux de contrôle de l'ensemble du programme est facilement perturbé par une action mal conduite au milieu). Nous ferions mieux de rendre cette exigence explicite. Eh bien, il s’avère que ce sont les lois de la monade, bien que je ne sois pas sûr de pouvoir les formuler sans les opérateurs standard bind/join.

Quoi qu'il en soit, nous avons maintenant atteint une formulation de IO qui possède une instance monad appropriée:

data IO₄ a = TxtOut String (IO₄ a)
           | TxtIn (String -> IO₄ a)
           | TerminateWith a

txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith

instance Functor IO₄ where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO₄ where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO₄ where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

Évidemment, ce n'est pas une mise en œuvre efficace d'IO, mais c'est en principe utilisable.

19
leftaroundabout

Les monades servent essentiellement à composer des fonctions ensemble dans une chaîne. Période.

Maintenant, leur composition diffère d'une monade à l'autre, ce qui entraîne différents comportements (par exemple, pour simuler un état mutable dans la monade d'état).

La confusion à propos des monades est qu’elles sont si générales, c’est-à-dire un mécanisme permettant de composer des fonctions, elles peuvent être utilisées pour beaucoup de choses, laissant ainsi croire aux gens que les monades concernent l’état, l’IO, etc. ".

Maintenant, une chose intéressante à propos des monades est que le résultat de la composition est toujours du type "M a", c'est-à-dire une valeur à l'intérieur d'une enveloppe étiquetée avec "M". Cette fonctionnalité s'avère être vraiment agréable à mettre en oeuvre, par exemple, une séparation claire entre code pur et code impur: déclarez toutes les actions impures en tant que fonctions de type "IO a" et ne fournissez aucune fonction lors de la définition du IO monad , pour sortir le "a" de l'intérieur du "IO a". Le résultat est qu'aucune fonction ne peut être pure et en même temps supprimer une valeur d'un "IO a", car il n'y a aucun moyen de prendre une telle valeur tout en restant pure (la fonction doit être dans la monade "IO" pour pouvoir être utilisée). telle valeur). (NOTE: eh bien, rien n’est parfait, donc le "camisole de force IO" peut être cassé avec "unsafePerformIO: IO a -> a", polluant ainsi ce qui était supposé être une fonction pure, mais cela devrait être utilisé très modérément et quand vous savez vraiment ne pas introduire de code impur avec des effets secondaires.

3
mljrg

Monads ne sont qu'un cadre pratique pour résoudre une classe de problèmes récurrents. Premièrement, les monades doivent être foncteurs (c’est-à-dire qu'elles doivent prendre en charge la cartographie sans regarder les éléments (ou leur type)), elles doivent également apporter une opération obligatoire (ou chaînage) et un moyen de créer une valeur monadique à partir d’un type d’élément (return). Enfin, bind et return doivent satisfaire deux équations (identités gauche et droite), également appelées lois de la monade. (Vous pouvez également définir des monades pour avoir un flattening operation au lieu d'une liaison.)

La liste monade est couramment utilisée pour traiter le non-déterminisme. L’opération bind sélectionne un élément de la liste (tous intuitivement dans mondes parallèles), permet au programmeur de faire des calculs avec eux, puis combine les résultats de tous les mondes en une seule liste (en concaténant , ou aplatissement, une liste imbriquée). Voici comment on définirait une fonction de permutation dans le cadre monadique de Haskell:

perm [e] = [[e]]
perm l = do (leader, index) <- Zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

Voici un exemple repl session:

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

Il convient de noter que la liste monade n'est en aucun cas un calcul effectuant de côté. Une structure mathématique étant une monade (c'est-à-dire conforme aux interfaces et aux lois susmentionnées) n'implique pas d'effets secondaires, bien que les phénomènes d'effet secondaire s'insèrent souvent parfaitement dans le cadre monadique.

3
heisenbug

Vous avez besoin de monades si vous avez un constructeur du type et fonctions qui retournent des valeurs de cette famille de type. Finalement, vous voudriez combiner ces types de fonctions ensemble. Ce sont les trois éléments clés pour répondre pourquoi .

Laissez-moi élaborer. Vous avez Int, String et Real et des fonctions de type Int -> String, String -> Real et ainsi de suite. Vous pouvez facilement combiner ces fonctions en terminant par Int -> Real. La vie est belle.

Ensuite, un jour, vous devez créer une nouvelle famille de types. Cela peut être dû au fait que vous devez envisager la possibilité de ne renvoyer aucune valeur (Maybe), de renvoyer une erreur (Either), de multiples résultats (List), etc.

Notez que Maybe est un constructeur de type. Il prend un type, comme Int et renvoie un nouveau type Maybe Int. Première chose à retenir, pas de constructeur de type, pas de monade.

Bien sûr, vous voulez utiliser votre constructeur de type dans votre code, et vous vous retrouverez bientôt avec des fonctions telles que Int -> Maybe String et String -> Maybe Float. Maintenant, vous ne pouvez pas facilement combiner vos fonctions. La vie n'est plus bonne.

Et voici quand les monades viennent à la rescousse. Ils vous permettent de combiner à nouveau ce type de fonctions. Il vous suffit de modifier la composition . pour > ==.

2
jdinunzio