Pourquoi avons-nous besoin de monades?

367

À 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?

cibercitizen1
la source
4
Quelles recherches avez-vous déjà faites? Où as-tu regardé? Quelles ressources avez-vous trouvées? Nous nous attendons à ce que vous fassiez une quantité importante de recherches avant de demander, et montrez-nous dans la question quelles recherches vous avez faites . Il existe de nombreuses ressources qui tentent d'expliquer la motivation des ressources - si vous n'en avez pas trouvé du tout, vous devrez peut-être faire un peu plus de recherche. Si vous en avez trouvé mais qu'ils ne vous ont pas aidé, cela ferait une meilleure question si vous expliquiez ce que vous aviez trouvé et pourquoi spécifiquement ils ne fonctionnaient pas pour vous.
DW
8
C'est certainement un meilleur ajustement pour Programmers.StackExchange et pas un bon ajustement pour StackOverflow. Je voterais pour migrer si je le pouvais, mais je ne peux pas. = (
jpmc26
3
@ jpmc26 Très probablement, il y serait fermé comme "principalement basé sur l'opinion"; ici, il y a au moins une chance (comme le montre le grand nombre de votes positifs, la réouverture rapide hier, et plus de votes serrés pour l'instant)
Izkata

Réponses:

581

Pourquoi avons-nous besoin de monades?

  1. Nous voulons programmer uniquement en utilisant des 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 un programme ) en n'utilisant que des fonctions ?

    Solution: composer des fonctions . Si vous voulez d'abord get ensuite f, écrivez f(g(x,y)). De cette façon, « le programme » est une fonction aussi bien: main = f(g(x,y)). OK mais ...

  3. 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), autorisons g : Real,Real -> Real | Nothing(fonction de deux réels en (réel ou rien)).

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

  5. Qu'arrive-t-il maintenant f(g(x,y))? fn'est pas prêt à consommer a Maybe Real. Et, nous ne voulons pas changer toutes les fonctions avec glesquelles nous pourrions nous connecter pour consommer a Maybe 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 / composez gà f). Nous voulons >>=obtenir gla sortie de, l'inspecter et, au cas où ce ne serait Nothingtout simplement pas appeler fet retourner Nothing; ou au contraire, extrayez la boîte Realet nourrissez f-la. (Cet algorithme n'est que l'implémentation de >>=pour le Maybetype). Notez également qu'il ne >>=doit être écrit qu'une seule fois par "type de boxe" (case différente, algorithme d'adaptation différent).

  6. 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 gcelle-ci renvoient ces "valeurs encadrées". 2. Avoir un compositeur / éditeur g >>= fde liens pour aider à connecter gla sortie de fl'entrée, de sorte que nous ne devons pas en changer fdu tout.

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

Un bonheur total!

cibercitizen1
la source
64
@Carl S'il vous plaît, écrivez une meilleure réponse pour nous éclairer
XrXr
15
@Carl Je pense qu'il est clair dans la réponse qu'il y a beaucoup de problèmes qui bénéficient de ce schéma (point 6) et que la IOmonade n'est qu'un problème de plus dans la liste IO(point 7). D'un autre côté, IOn'apparaît qu'une seule fois et à la fin, donc, ne comprenez pas que vous "parliez la plupart du temps ... des E / S".
cibercitizen1
4
Les grandes idées fausses sur les monades: les monades sur l'état; monades sur la gestion des exceptions; il n'y a aucun moyen d'implémenter IO en FPL pur sans monades; les monades sont sans ambiguïté (le contre-argument l'est Either). La réponse la plus importante concerne "Pourquoi avons-nous besoin de foncteurs?".
vlastachu
4
"6. 2. Avoir un compositeur / éditeur g >>= fde liens pour aider à connecter gla sortie de fl'entrée, afin que nous n'ayons pas à en changer fdu tout." ce n'est pas juste du tout. Avant, en f(g(x,y)), fpouvait produire quoi que ce soit. Ça pourrait l'être f:: Real -> String. Avec la "composition monadique", elle doit être modifiée pour produire Maybe 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.
Will Ness
3
Votre réponse est juste dans le sens où les monades IMO sont en effet mieux décrites comme étant sur la composition / alité des "fonctions" (les flèches Kleisli vraiment), mais les détails précis de quel type va où sont ce qui en fait des "monades". vous pouvez câbler les boîtes de toutes sortes de manières (comme Functor, etc.). Cette façon spécifique de les relier entre eux est ce qui définit "la monade".
Will Ness
220

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 IOtype s'occupe très bien de lui-même. La désucrage monadique actuelle des doblocs pourrait être remplacé par désucrage à bindIO, returnIOet failIOtel que défini dans le GHC.Basemodule. (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)et bar :: (a -> Maybe b)? foo . barne vérifie pas le type, bet Maybe bn'est pas du même type.

Mais ... c'est presque vrai. Vous voulez juste un peu de latitude. Vous voulez pouvoir traiter Maybe bcomme si c'était fondamentalement b. 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 (.)et idforment 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 pour Maybeconsisterait en (.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)et id :: a -> Maybe a. Autrement dit, les objets de la catégorie augmentent le (->)avec un Maybe, (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 juste Maybe. Ils travaillent avec tous les types qui peuvent assembler une catégorie appropriée, en respectant les lois de catégorie.

  1. Identité gauche: id . f=f
  2. Identité correcte: f . id=f
  3. Associativité: 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« s returnest la même que Kleisli id. 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 Monadabstraction 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.

Carl
la source
2
Pouvez-vous expliquer la relation entre les catégories en général et les catégories de Kleisli? Les trois lois que vous décrivez s'appliquent à toutes les catégories.
dfeuer
1
@dfeuer Oh. Pour le mettre dans le code, newtype Kleisli m a b = Kleisli (a -> m b). Les catégories de Kleisli sont des fonctions où le type de retour catégoriel ( bdans ce cas) est l'argument d'un constructeur de type m. Iff Kleisli mforme une catégorie, mest une Monade.
Carl
1
Qu'est-ce qu'un type de retour catégorique exactement? Kleisli msemble former une catégorie dont les objets sont de type Haskell et tels que les flèches de aà bsont les fonctions de aà m b, avec id = returnet (.) = (<=<). Est-ce à peu près juste, ou est-ce que je mélange différents niveaux de choses ou quelque chose?
dfeuer
1
@dfeuer C'est exact. Les objets sont tous de types, et les morphismes sont entre les types aet b, mais ce ne sont pas de simples fonctions. Ils sont décorés d'un extra mdans la valeur de retour de la fonction.
Carl
1
La terminologie de la théorie des catégories est-elle vraiment nécessaire? Peut-être que Haskell serait plus facile si vous transformiez les types en images où le type serait l'ADN pour la façon dont les images sont dessinées (types dépendants cependant *), puis vous utilisez l'image pour écrire votre programme avec les noms étant de petits caractères rubis au-dessus de l'icône.
aoeu256
24

Benjamin Pierce a déclaré 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 dans un programme.

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 filterfonction 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 tout iavoir i <= 10, sum [1..i] > 4, sum [1..i] < 25, on peut écrire

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

ce qui équivaut [3,4,5,6].

Ou nous pouvons redéfinir la nubfonction, 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 ici comme un accumulateur. Le code fonctionne, car il est possible de quitter la liste monade, donc le calcul entier reste pur ( notElemn'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 du Control.Monadmodule:

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)

filterMexécute une action monadique pour tous les éléments d'une liste, produisant des éléments, pour lesquels l'action monadique revient 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 la 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 comme illustration finale, filterAccumpeut être défini en termes de filterM:

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

avec la StateTmonade, 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.

user3237465
la source
1
Cette réponse explique pourquoi nous avons besoin de la classe de types Monad. La meilleure façon de comprendre pourquoi nous avons besoin de monades et pas d'autre chose est de lire la différence entre les monades et les foncteurs applicatifs: un , deux .
user3237465
21

Je ne pense pas que cela IOdevrait ê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:

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

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?

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

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

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.

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, 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 seulement Output. Cela inclut certainement une sortie de 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 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.

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

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)

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:

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 implémentation efficace des E / S, mais elle est en principe utilisable.

à gauche
la source
@jdlugosz: 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.
leftaroundabout
4

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, bindet returndoit satisfaire deux équations (identités gauche et droite), également appelées lois de la monade. (Alternativement, on pourrait définir les monades comme ayant au flattening operationlieu 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:

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 de session de repl :

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

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.

heisenbug
la source
3

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"

mljrg
la source
2

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, Stringet Realet des fonctions de type Int -> String, String -> Realet ainsi de suite. Vous pouvez combiner ces fonctions facilement, en terminant par Int -> 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 Maybes'agit d'un constructeur de type. Il prend un type, comme Intet retourne 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 comme Int -> Maybe Stringet String -> 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 > == .

jdinunzio
la source
2
Cela n'a rien à voir avec les familles de types. De quoi parlez-vous réellement?
dfeuer