À mon humble avis, les réponses à la fameuse question "Qu'est-ce qu'une monade?" , en particulier les plus votés, essayez d'expliquer ce qu'est une monade sans expliquer clairement pourquoi les monades sont vraiment nécessaires . Peut-on l'expliquer comme la solution à un problème?
367
Réponses:
Pourquoi avons-nous besoin de monades?
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 un programme ) en n'utilisant que des fonctions ?
Solution: composer des fonctions . Si vous voulez d'abord
g
et ensuitef
, écrivezf(g(x,y))
. De cette façon, « le programme » est une fonction aussi bien:main = f(g(x,y))
. OK mais ...Plus de problèmes: certaines fonctions peuvent échouer (c. -à -d
g(2,0)
. Diviser par 0). Nous n'avons pas d '"exceptions" dans FP (une exception n'est pas une fonction). Comment le résout-on?Solution: Permettons aux fonctions de renvoyer deux types de choses : au lieu d'avoir
g : Real,Real -> Real
(fonction de deux réels en réel), autorisonsg : Real,Real -> Real | Nothing
(fonction de deux réels en (réel ou rien)).Mais les fonctions ne devraient (pour être plus simples) retourner qu'une seule chose .
Solution: créons un nouveau type de données à renvoyer, un " type boxe " qui renferme peut-être un réel ou tout simplement rien. Par conséquent, nous pouvons avoir
g : Real,Real -> Maybe Real
. OK mais ...Qu'arrive-t-il maintenant
f(g(x,y))
?f
n'est pas prêt à consommer aMaybe Real
. Et, nous ne voulons pas changer toutes les fonctions avecg
lesquelles nous pourrions nous connecter pour consommer aMaybe Real
.Solution: ayons une fonction spéciale pour "connecter" / "composer" / "lier" les fonctions . De cette façon, nous pouvons, en arrière-plan, adapter la sortie d'une fonction pour alimenter la suivante.
Dans notre cas:
g >>= f
(connectez / composezg
àf
). Nous voulons>>=
obtenirg
la sortie de, l'inspecter et, au cas où ce ne seraitNothing
tout simplement pas appelerf
et retournerNothing
; ou au contraire, extrayez la boîteReal
et nourrissezf
-la. (Cet algorithme n'est que l'implémentation de>>=
pour leMaybe
type). Notez également qu'il ne>>=
doit être écrit qu'une seule fois par "type de boxe" (case différente, algorithme d'adaptation différent).De nombreux autres problèmes surviennent qui peuvent être résolus en utilisant ce même modèle: 1. Utilisez une "boîte" pour codifier / stocker différentes significations / valeurs, et avoir des fonctions comme
g
celle-ci renvoient ces "valeurs encadrées". 2. Avoir un compositeur / éditeurg >>= f
de liens pour aider à connecterg
la sortie def
l'entrée, de sorte que nous ne devons pas en changerf
du tout.Les problèmes remarquables qui peuvent être résolus en utilisant cette technique sont:
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»: des fonctions qui produisent des sorties différentes pour la même entrée. Par conséquent, marquons ces fonctions, en les faisant retourner une valeur balisée / encadrée:
IO
monad.Un bonheur total!
la source
IO
monade n'est qu'un problème de plus dans la listeIO
(point 7). D'un autre côté,IO
n'apparaît qu'une seule fois et à la fin, donc, ne comprenez pas que vous "parliez la plupart du temps ... des E / S".Either
). La réponse la plus importante concerne "Pourquoi avons-nous besoin de foncteurs?".g >>= f
de liens pour aider à connecterg
la sortie def
l'entrée, afin que nous n'ayons pas à en changerf
du tout." ce n'est pas juste du tout. Avant, enf(g(x,y))
,f
pouvait produire quoi que ce soit. Ça pourrait l'êtref:: Real -> String
. Avec la "composition monadique", elle doit être modifiée pour produireMaybe String
, sinon les types ne conviendront pas. De plus,>>=
lui - même ne rentre pas !! C'est>=>
ça qui fait cette composition, non>>=
. Voir la discussion avec dfeuer sous la réponse de Carl.La réponse est, bien sûr, "Nous ne le faisons pas" . Comme pour toutes les abstractions, ce n'est pas nécessaire.
Haskell n'a pas besoin d'une abstraction de monade. Il n'est pas nécessaire pour effectuer des IO dans un langage pur. Le
IO
type s'occupe très bien de lui-même. La désucrage monadique actuelle desdo
blocs pourrait être remplacé par désucrage àbindIO
,returnIO
etfailIO
tel que défini dans leGHC.Base
module. (Ce n'est pas un module documenté sur le piratage, donc je vais devoir pointer sa source pour la documentation.) Donc non, il n'y a pas besoin de l'abstraction de la monade.Alors 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 du code qui fonctionne sur toutes les instances de cette structure. Pour le dire de manière plus concise - réutilisation du code.
Dans les langages fonctionnels, l'outil le plus puissant trouvé pour la réutilisation de code a été la composition des fonctions. Le bon vieil
(.) :: (b -> c) -> (a -> b) -> (a -> c)
opérateur est extrêmement puissant. Il permet d'écrire facilement de minuscules fonctions et de les coller ensemble avec un minimum de surcharge syntaxique ou sémantique.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)
etbar :: (a -> Maybe b)
?foo . bar
ne vérifie pas le type,b
etMaybe b
n'est pas du même type.Mais ... c'est presque vrai. Vous voulez juste un peu de latitude. Vous voulez pouvoir traiter
Maybe b
comme si c'était fondamentalementb
. C'est une mauvaise idée de simplement les traiter comme du même type. C'est plus ou moins la même chose que les pointeurs nuls, ce que Tony Hoare a appelé célèbre l'erreur d'un milliard de dollars . Donc, si vous ne pouvez pas les traiter comme du même type, vous pouvez peut-être trouver un moyen d'étendre le mécanisme de composition(.)
.Dans ce cas, il est important d'examiner vraiment la théorie sous-jacente
(.)
. Heureusement, quelqu'un l'a déjà fait pour nous. Il s'avère que la combinaison de(.)
etid
forment une construction mathématique connue sous le nom de catégorie . Mais il existe d'autres façons de former des catégories. Une catégorie Kleisli, par exemple, permet d'augmenter un peu les objets en cours de composition. Une catégorie Kleisli pourMaybe
consisterait en(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
etid :: a -> Maybe a
. Autrement dit, les objets de la catégorie augmentent le(->)
avec unMaybe
,(a -> b)
devient ainsi(a -> Maybe b)
.Et soudain, 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 Kleisli fonctionnent avec plus de types que justeMaybe
. Ils travaillent avec tous les types qui peuvent assembler une catégorie appropriée, en respectant les lois de catégorie.id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
Tant que vous pouvez prouver que votre type obéit à ces trois lois, vous pouvez le transformer en catégorie Kleisli. Et quel est le gros problème à ce sujet? Eh bien, il s'avère que les monades sont exactement la même chose que les catégories Kleisli.
Monad
« sreturn
est la même que Kleisliid
.Monad
« s(>>=)
est pas identique à Kleisli(.)
, mais il se révèle être très facile d'écrire chacun en termes de l'autre. Et les lois de catégorie sont les mêmes que les lois de monade, lorsque vous les traduisez à travers la différence entre(>>=)
et(.)
.Alors pourquoi traverser tout ça? Pourquoi avoir une
Monad
abstraction dans la langue? Comme je l'ai mentionné ci-dessus, il permet la réutilisation du code. Il permet même la réutilisation du code selon deux dimensions différentes.La première dimension de la réutilisation du code vient 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 composé de boucles qui fonctionnent avec n'importe quelle instance de
Monad
.La deuxième dimension est indirecte, mais elle découle de l'existence de la composition. Lorsque la composition est facile, il est naturel d'écrire du code en petits morceaux réutilisables. De la même manière, avoir l'
(.)
opérateur pour les fonctions encourage l'écriture de petites fonctions réutilisables.Alors pourquoi l'abstraction existe-t-elle? Parce qu'il s'est avéré être un outil qui permet plus de composition dans le code, résultant en la création de code réutilisable et en encourageant la création de code plus réutilisable. La réutilisation du code est l'un des Saint Graal de la programmation. L'abstraction de la monade existe parce qu'elle nous déplace un peu vers ce Saint Graal.
la source
newtype Kleisli m a b = Kleisli (a -> m b)
. Les catégories de Kleisli sont des fonctions où le type de retour catégoriel (b
dans ce cas) est l'argument d'un constructeur de typem
. IffKleisli m
forme une catégorie,m
est une Monade.Kleisli m
semble former une catégorie dont les objets sont de type Haskell et tels que les flèches dea
àb
sont les fonctions dea
àm b
, avecid = return
et(.) = (<=<)
. Est-ce à peu près juste, ou est-ce que je mélange différents niveaux de choses ou quelque chose?a
etb
, mais ce ne sont pas de simples fonctions. Ils sont décorés d'un extram
dans la valeur de retour de la fonction.Benjamin Pierce a déclaré dans TAPL
C'est pourquoi un langage équipé d'un système de typage puissant est strictement plus expressif, qu'un langage mal typé. Vous pouvez penser aux monades de la même manière.
En tant que point @Carl et sigfpe , vous pouvez équiper un type de données avec toutes les opérations que vous souhaitez sans recourir à des monades, des classes de caractères ou tout autre élément abstrait. Cependant, les monades vous permettent non seulement d'écrire du code réutilisable, mais aussi d'abstraire tous les détails redondants.
Par exemple, disons que nous voulons filtrer une liste. La manière la plus simple est d'utiliser la
filter
fonctionfilter (> 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, estPour tout
i
avoiri <= 10, sum [1..i] > 4, sum [1..i] < 25
, on peut écrirece qui équivaut
[3,4,5,6]
.Ou nous pouvons redéfinir la
nub
fonction, qui supprime les éléments en double d'une liste, en termes defilterAccum
: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 ici comme un accumulateur. Le code fonctionne, car il est possible de quitter la liste monade, donc le calcul entier reste pur (notElem
n'utilise pas>>=
réellement, mais il pourrait). Cependant, il n'est pas possible de quitter la monade IO en toute sécurité (c'est-à-dire que vous ne pouvez pas exécuter une action IO et renvoyer une valeur pure - la valeur sera toujours enveloppée dans la monade IO). Un autre exemple est les tableaux mutables: après avoir quitté la monade ST, où un tableau mutable est actif, vous ne pouvez plus mettre à jour le tableau en temps constant. Nous avons donc besoin d'un filtrage monadique duControl.Monad
module:filterM
exécute une action monadique pour tous les éléments d'une liste, produisant des éléments, pour lesquels l'action monadique revientTrue
.Un exemple de filtrage avec un tableau:
imprime
[1,2,4,5,3,8,9]
comme prévu.Et une version avec la monade IO, qui demande quels éléments retourner:
Par exemple
Et comme illustration finale,
filterAccum
peut être défini en termes defilterM
:avec la
StateT
monade, qui est utilisée sous le capot, étant juste un type de données ordinaire.Cet exemple illustre que les monades vous permettent non seulement d'abstraire le contexte de calcul et d'écrire du code réutilisable propre (en raison de la composabilité des monades, comme l'explique @Carl), mais également de traiter uniformément les types de données définis par l'utilisateur et les primitives intégrées.
la source
Je ne pense pas que cela
IO
devrait être considéré comme une monade particulièrement exceptionnelle, mais c'est certainement l'une des plus étonnantes pour les débutants, je vais donc l'utiliser pour mon explication.Construire naïvement un système d'E / S pour Haskell
Le système d'E / S le plus simple et imaginable pour un langage purement fonctionnel (et en fait celui avec lequel Haskell a commencé) est le suivant:
Avec la paresse, cette simple signature suffit pour construire des programmes de terminaux interactifs - très limités cependant. Le plus frustrant est que nous ne pouvons produire que du texte. Et si nous ajoutions des possibilités de sortie plus intéressantes?
mignon, mais bien sûr, une «sortie alternative» beaucoup plus réaliste consisterait à écrire dans un fichier . Mais alors vous voudriez aussi un moyen de lire des fichiers. Une chance?
Eh bien, lorsque nous prenons notre
main₁
programme et que nous transmettons simplement un fichier au processus (en utilisant les fonctionnalités du système d'exploitation), nous avons essentiellement implémenté la lecture de fichiers. Si nous pouvions déclencher cette lecture de fichier à partir du langage Haskell ...Cela utiliserait un «programme interactif»
String->[Output]
, lui fournirait une chaîne obtenue à partir d'un fichier et donnerait un programme non interactif qui exécute simplement celui donné.Il y a un problème ici: nous ne savons pas vraiment quand le fichier est lu. La
[Output]
liste donne certainement un bon ordre aux sorties , mais nous n'obtenons pas d'ordre pour quand les entrées seront effectuées.Solution: créez des entrées-événements également dans la liste des choses à faire.
Ok, maintenant vous pouvez remarquer un déséquilibre: vous pouvez lire un fichier et en faire une sortie dépendante, mais vous ne pouvez pas utiliser le contenu du fichier pour décider par exemple de lire également un autre fichier. Solution évidente: rendre le résultat des événements d'entrée aussi quelque chose de type
IO
, pas seulementOutput
. Cela inclut certainement une sortie de texte simple, mais permet également de lire des fichiers supplémentaires, etc.Cela vous permettrait désormais d'exprimer n'importe quelle opération de fichier que vous pourriez souhaiter dans un programme (mais peut-être pas avec de bonnes performances), mais c'est un peu trop compliqué:
main₃
donne toute une liste d'actions. Pourquoi n'utilisons-nous pas simplement la signature:: IO₁
, qui a ceci comme cas spécial?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. Donc, nous pourrions aussi bien abandonner la structure de la liste, et simplement contre «et ensuite faire» à chaque opération de sortie.
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 devrait y avoir un bon couple de ces constructeurs fondamentaux, mais pour la plupart des choses de niveau supérieur, nous aimerions écrire une fonction avec une belle signature de haut niveau. Il s'avère que la plupart de ces éléments semblent assez similaires: acceptent une sorte de valeur significativement typée et produisent une action d'E / S comme résultat.
Il y a évidemment un modèle ici, et nous ferions mieux de l'écrire comme
Maintenant, cela commence à sembler familier, mais nous ne traitons toujours que des fonctions simples à peine déguisées sous le capot, et c'est risqué: chaque «action de valeur» a la responsabilité de transmettre 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 suis pas sûr que nous puissions vraiment les formuler sans les opérateurs standard de liaison / jointure.
En tout cas, nous avons maintenant atteint une formulation d'E / S qui a une instance de monade appropriée:
Évidemment, ce n'est pas une implémentation efficace des E / S, mais elle est en principe utilisable.
la source
IO3 a ≡ Cont IO2 a
. Mais je voulais dire ce commentaire plus comme un clin d'œil à ceux qui connaissent déjà la monade de continuation, car elle n'a pas exactement la réputation d'être adaptée aux débutants.Les monades ne sont qu'un cadre pratique pour résoudre une classe de problèmes récurrents. Tout d'abord, les monades doivent être des foncteurs (c'est-à-dire qu'elles doivent prendre en charge le mappage sans regarder les éléments (ou leur type)), elles doivent également apporter une opération de liaison (ou de chaînage) et un moyen de créer une valeur monadique à partir d'un type d'élément (
return
). Enfin,bind
etreturn
doit satisfaire deux équations (identités gauche et droite), également appelées lois de la monade. (Alternativement, on pourrait définir les monades comme ayant auflattening operation
lieu de se lier.)La monade de liste est couramment utilisée pour traiter le non-déterminisme. L'opération de liaison sélectionne un élément de la liste (intuitivement tous dans des mondes parallèles ), permet au programmeur de faire des calculs avec eux, puis combine les résultats dans tous les mondes à une seule liste (en concaténant ou en aplatissant une liste imbriquée) ). Voici comment on définirait une fonction de permutation dans le cadre monadique de Haskell:
Voici un exemple de session de repl :
Il est à noter que la monade de liste n'est en aucun cas un calcul d'effet secondaire. Une structure mathématique étant une monade (c'est-à-dire conforme aux interfaces et aux lois mentionnées ci-dessus) n'implique pas d'effets secondaires, bien que les phénomènes d'effet secondaire s'intègrent souvent bien dans le cadre monadique.
la source
Les monades servent essentiellement à composer des fonctions ensemble dans une chaîne. Période.
Maintenant, la façon dont ils composent diffère selon les monades existantes, entraînant ainsi des comportements différents (par exemple, pour simuler un état mutable dans la monade d'état).
La confusion à propos des monades est qu'étant si générales, c'est-à-dire un mécanisme pour composer des fonctions, elles peuvent être utilisées pour beaucoup de choses, amenant ainsi les gens à croire que les monades concernent l'état, les entrées-sorties, etc., alors qu'elles ne concernent que la "composition de fonctions" ".
Maintenant, une chose intéressante à propos des monades, c'est que le résultat de la composition est toujours de type "M a", c'est-à-dire une valeur à l'intérieur d'une enveloppe étiquetée avec "M". Cette fonctionnalité s'avère très agréable à implémenter, par exemple, une séparation claire entre le code pur et le code impur: déclarer toutes les actions impures comme des fonctions de type "IO a" et ne fournir aucune fonction, lors de la définition de la monade IO, pour supprimer le " une "valeur de l'intérieur du" IO a ". Le résultat est qu'aucune fonction ne peut être pure et en même temps extraire 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 à l'intérieur de la monade "IO" à utiliser une telle valeur). (REMARQUE: eh bien, rien n'est parfait, donc la "camisole de force IO" peut être cassée en utilisant "unsafePerformIO: IO a -> a"
la source
Vous avez besoin de monades si vous avez un constructeur de type et des fonctions qui retournent des valeurs de cette famille de types . Finalement, vous souhaitez combiner ce type de fonctions ensemble . Ce sont les trois éléments clés pour expliquer pourquoi .
Permettez-moi de développer. Vous avez
Int
,String
etReal
et des fonctions de typeInt -> String
,String -> Real
et ainsi de suite. Vous pouvez combiner ces fonctions facilement, en terminant parInt -> Real
. La vie est belle.Puis, un jour, vous devez créer une nouvelle famille de types . Cela peut être dû au fait que vous devez considérer la possibilité de ne renvoyer aucune valeur (
Maybe
), de renvoyer une erreur (Either
), plusieurs résultats (List
) et ainsi de suite.Notez qu'il
Maybe
s'agit d'un constructeur de type. Il prend un type, commeInt
et retourne un nouveau typeMaybe 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 comme
Int -> Maybe String
etString -> Maybe Float
. Maintenant, vous ne pouvez pas facilement combiner vos fonctions. La vie n'est plus belle.Et voici quand les monades viennent à la rescousse. Ils vous permettent de combiner à nouveau ce type de fonctions. Vous avez juste besoin de changer la composition . pour > == .
la source