Pourquoi des monades? Comment résout-il les effets secondaires?

J’apprends le Haskell et j’essaie de comprendre les Monades. J’ai 2 questions

D’après ce que j’ai compris, Monad n’est qu’une autre classe qui déclare des moyens d’interagir avec les données à l’intérieur de «conteneurs», y compris Maybes, Lists et IOs. Il semble astucieux et propre d’implémenter ces 3 choses avec un seul concept, mais en réalité, le problème est qu’il peut y avoir une gestion des erreurs nette dans une chaîne de fonctions, de conteneurs et d’effets secondaires. Est-ce une interprétation correcte?

Deuxièmement, comment le problème des effets secondaires est-il résolu? Avec ce concept de conteneur, le langage dit essentiellement que tout ce qui est contenu dans les conteneurs est non déterministe (tel que les E / S). Comme les listes et les entrées-sorties sont des conteneurs, les listes sont classées en équivalence avec IO, même si les valeurs à l’intérieur des listes me semblent assez déterministes. Alors, qu’est-ce qui est déterministe et quels sont les effets secondaires? Je ne peux pas me tromper sur l’idée qu’une valeur de base est déterministe, jusqu’à ce que vous la colliez dans un conteneur (qui n’est pas spécial que la même valeur avec d’autres valeurs à côté, par exemple Rien) et il peut maintenant être aléatoire .

Quelqu’un peut-il expliquer comment, intuitivement, Haskell se débrouille avec l’évolution de l’état avec les entrées et les sorties? Je ne vois pas la magie ici.

Le point est donc qu’il peut y avoir une gestion des erreurs propre dans une chaîne de fonctions, de conteneurs et d’effets secondaires. Est-ce une interprétation correcte?

Pas vraiment. Vous avez mentionné beaucoup de concepts que les gens citent lorsqu’ils tentent d’expliquer les monades, y compris les effets secondaires, la gestion des erreurs et le non-déterminisme, mais vous avez l’impression que tous ces concepts s’appliquent à toutes les monades. Mais il y a un concept que vous avez mentionné: l’ enchaînement .

Il y a deux saveurs différentes de ceci, donc je l’expliquerai de deux manières différentes: une sans effets secondaires et une avec des effets secondaires.

Pas d’effets secondaires:

Prenons l’exemple suivant:

 addM :: (Monad m, Num a) => ma -> ma -> ma addM ma mb = do a <- ma b <- mb return (a + b) 

Cette fonction ajoute deux nombres, avec la torsion qu'ils sont enveloppés dans une monade. Quelle monade? N'a pas d'importance! Dans tous les cas, cette syntaxe spéciale permet de supprimer les éléments suivants:

 addM ma mb = ma >>= \a -> mb >>= \b -> return (a + b) 

... ou, avec la priorité de l'opérateur rendue explicite:

 ma >>= (\a -> mb >>= (\b -> return (a + b))) 

Maintenant, vous pouvez vraiment voir qu'il s'agit d'une chaîne de petites fonctions, toutes composées ensemble, et son comportement dépendra de la façon dont >>= et le return sont définis pour chaque monade. Si vous êtes familier avec le polymorphism dans les langages orientés object, c'est essentiellement la même chose: une interface commune avec plusieurs implémentations. C'est légèrement plus déconcertant que votre interface OOP moyenne, car l'interface représente une politique de calcul plutôt qu'un animal, une forme ou autre.

Bon, voyons quelques exemples de la façon dont addM se comporte à travers les différentes monades. La monade d' Identity est un endroit décent pour commencer, puisque sa définition est sortingviale:

 instance Monad Identity where return a = Identity a -- create an Identity value (Identity a) >>= f = fa -- apply f to a 

Alors que se passe-t-il quand on dit:

 addM (Identity 1) (Identity 2) 

En élargissant cette étape par étape:

 (Identity 1) >>= (\a -> (Identity 2) >>= (\b -> return (a + b))) (\a -> (Identity 2) >>= (\b -> return (a + b)) 1 (Identity 2) >>= (\b -> return (1 + b)) (\b -> return (1 + b)) 2 return (1 + 2) Identity 3 

Génial. Maintenant, puisque vous avez mentionné la gestion des erreurs propres, regardons la monade Maybe . Sa définition n'est que légèrement plus délicate que l' Identity :

 instance Monad Maybe where return a = Just a -- same as Identity monad! (Just a) >>= f = fa -- same as Identity monad again! Nothing >>= _ = Nothing -- the only real difference from Identity 

Donc, vous pouvez imaginer que si nous disons addM (Just 1) (Just 2) nous aurons Just 3 . Mais pour les sourires, développons addM Nothing (Just 1) place:

 Nothing >>= (\a -> (Just 1) >>= (\b -> return (a + b))) Nothing 

Ou l'inverse, addM (Just 1) Nothing :

 (Just 1) >>= (\a -> Nothing >>= (\b -> return (a + b))) (\a -> Nothing >>= (\b -> return (a + b)) 1 Nothing >>= (\b -> return (1 + b)) Nothing 

Donc, la définition de >>= indépendante Maybe été modifiée pour tenir compte de l'échec. Lorsqu'une fonction est appliquée à une valeur Maybe utilisant >>= , vous obtenez ce que vous attendez.

Ok, alors vous avez mentionné le non-déterminisme. Oui, la liste des monades peut être considérée comme modélisant le non-déterminisme dans un sens ... C'est un peu bizarre, mais pensez à la liste comme représentant des valeurs possibles: [1, 2, 3] n'est pas une collection, c'est un nombre unique non déterministe qui pourrait être un, deux ou trois. Cela semble bête, mais cela commence à avoir un sens lorsque vous pensez à la façon dont >>= est défini pour les listes: il applique la fonction donnée à chaque valeur possible. Donc, addM [1, 2] [3, 4] va réellement calculer toutes les sums possibles de ces deux valeurs non déterministes: [4, 5, 5, 6] .

OK, maintenant pour répondre à votre deuxième question ...

Effets secondaires:

Disons que vous appliquez addM à deux valeurs dans la monade IO , comme:

 addM (return 1 :: IO Int) (return 2 :: IO Int) 

Vous n'obtenez rien de spécial, juste 3 dans la monade IO . addM ne lit ni n'écrit aucun état mutable, c'est donc plutôt amusant. Même chose pour les monades d' State ou ST . Pas drôle. Alors, utilisons une fonction différente:

 fireTheMissiles :: IO Int -- returns the number of casualties 

De toute évidence, le monde sera différent chaque fois que des missiles seront tirés. Clairement. Maintenant, supposons que vous essayiez d'écrire du code de tir non-missile totalement inoffensif et sans effets secondaires. Peut-être essayez-vous encore une fois d'append deux chiffres, mais cette fois sans aucune monade:

 add :: Num a => a -> a -> a add ab = a + b 

et tout à coup, votre main glisse et vous accidentellement typo:

 add ab = a + b + fireTheMissiles 

Une erreur honnête, vraiment. Les clés étaient si proches les unes des autres. Heureusement, parce que fireTheMissiles était de type IO Int plutôt que simplement Int , le compilateur est capable d'éviter une catastrophe.

Ok, exemple totalement inventé, mais le fait est que dans le cas des IO - IO , de la ST et des amis, le système de types garde les effets isolés dans un contexte spécifique. Cela n'élimine pas comme par magie les effets secondaires, ce qui rend le code référentiel transparent, ce qui ne devrait pas être le cas, mais il est clair au moment de la compilation de la scope des effets.

Donc, revenons au point d'origine: qu'est-ce que cela a à voir avec l'enchaînement ou la composition des fonctions? Eh bien, dans ce cas, c'est juste un moyen pratique d'exprimer une séquence d'effets:

 fireTheMissilesTwice :: IO () fireTheMissilesTwice = do a <- fireTheMissiles print a b <- fireTheMissiles print b 

Résumé:

Une monade représente une politique de chaînage des calculs. La politique d ' Identity est une composition de fonction pure. Maybe - Maybe la politique est la composition de fonction avec la propagation d' échec, la politique d ' IO est la composition de fonction impure et ainsi de suite.

Laissez-moi commencer par souligner l’excellent article ” Vous auriez pu inventer des monades “. Il illustre comment la structure Monad peut naturellement se manifester pendant que vous écrivez des programmes. Mais le tutoriel ne mentionne pas IO , alors je vais essayer de développer cette approche.

Commençons par ce que vous avez probablement déjà vu – la monade de conteneur. Disons que nous avons:

 f, g :: Int -> [Int] 

Une façon de voir cela est que cela nous donne un certain nombre de résultats possibles pour chaque entrée possible. Que faire si nous voulons toutes les sorties possibles pour la composition des deux fonctions? Donner toutes les possibilités possibles en appliquant les fonctions les unes après les autres?

Eh bien, il y a une fonction pour cela:

 fg x = concatMap g $ fx 

Si nous mettons cela plus général, nous obtenons

 fg x = fx >>= g xs >>= f = concatMap f xs return x = [x] 

Pourquoi voudrions-nous l’envelopper comme ça? Bien, écrire principalement nos programmes en utilisant >>= et return nous donne de belles propriétés – par exemple, nous pouvons être sûrs qu’il est relativement difficile d’oublier les solutions. Nous devrions explicitement le réintroduire, par exemple en ajoutant un autre skip fonction. Et aussi, nous avons maintenant une monade et pouvons utiliser tous les combinateurs de la bibliothèque monad!

Passons maintenant à votre exemple le plus difficile. Disons que les deux fonctions sont des “effets secondaires”. Ce n’est pas non déterministe, cela signifie simplement qu’en théorie, le monde entier est à la fois leur entrée (comme il peut les influencer) et leur sortie (car la fonction peut l’influencer). Nous obtenons donc quelque chose comme:

 f, g :: Int -> RealWorld# -> (Int, RealWorld#) 

Si nous voulons maintenant obtenir le monde qui nous rest, nous écrirons:

 fg x rw = let (y, rw') = fx rw (r, rw'') = gy rw' in (r, rw'') 

Ou généralisé:

 fg x = fx >>= g x >>= f = \rw -> let (y, rw') = x rw (r, rw'') = fy rw' in (r, rw'') return x = \rw -> (x, rw) 

Maintenant, si l’utilisateur ne peut utiliser que >>= , return et quelques valeurs d’ IO prédéfinies, nous obtenons à nouveau une bonne propriété: l’utilisateur ne verra jamais réellement le RealWorld# être passé! Et c’est une très bonne chose, car vous n’êtes pas vraiment intéressé par les détails de la getLine données de getLine . Et encore une fois, nous obtenons toutes les belles fonctions de haut niveau des bibliothèques monad.

Donc, les choses importantes à enlever:

  1. La monade capture les modèles courants dans votre code, comme “transmettez toujours tous les éléments du conteneur A au conteneur B” ou “transmettez ce tag réel”. Souvent, une fois que vous réalisez qu’il y a une monade dans votre programme, les choses compliquées deviennent simplement des applications du bon combinateur de monades.

  2. La monade vous permet de cacher complètement l’implémentation de l’utilisateur. C’est un excellent mécanisme d’encapsulation, que ce soit pour votre propre état interne ou pour la manière dont IO parvient à intégrer la non-pureté dans un programme pur de manière relativement sûre.


annexe

Dans le cas où quelqu’un est encore en train de se casser la tête sur RealWorld# autant que je l’ai fait au début: Il y a évidemment plus de magie après que toute l’abstraction de monade ait été supprimée. Le compilateur utilisera alors le fait qu’il ne peut y avoir qu’un seul “monde réel”. C’est une bonne et une mauvaise nouvelle:

  1. Il s’ensuit que le compilateur doit garantir l’ordre d’exécution entre les fonctions (ce que nous étions après!)

  2. Mais cela signifie aussi que le fait de faire passer le monde réel n’est pas nécessaire car il n’ya qu’un seul que nous pourrions éventuellement dire: celui qui est actuel lorsque la fonction est exécutée!

En fin de compte, une fois que l’ordre d’exécution est fixé, RealWorld# simplement optimisé. Par conséquent, les programmes utilisant la monade IO ont en réalité zéro surcharge à l’exécution. Notez également que l’utilisation de RealWorld# n’est évidemment qu’un moyen possible de placer les IOIO – mais il se trouve que c’est ce que GHC utilise en interne. Ce qui est bien avec les monades, c’est que l’utilisateur n’a pas besoin de savoir.

Vous pourriez voir une monade donnée m tant qu’ensemble / famille (ou domaine, domaine, etc.) d’ actions (pensez à une déclaration C). La monade m définit le type d’effets (latéraux) que ses actions peuvent avoir:

  • avec [] vous pouvez définir des actions qui peuvent exécuter leurs exécutions dans différents “mondes parallèles indépendants”;
  • avec Either Foo vous pouvez définir des actions qui peuvent échouer avec des erreurs de type Foo ;
  • avec IO vous pouvez définir des actions pouvant avoir des effets secondaires sur le “monde extérieur” (access aux fichiers, réseau, processus de lancement, faire un HTTP GET …);
  • vous pouvez avoir une monade dont l’effet est “randomness” (voir le paquetage MonadRandom );
  • Vous pouvez définir une monade dont les actions peuvent bouger dans un jeu (par exemple, les échecs, Go…) et recevoir un mouvement d’un adversaire mais ne peuvent pas écrire sur votre système de fichiers ou autre.

Résumé

Si m est une monade, ma est une action qui produit un résultat / sortie de type a .

Les opérateurs >> et >>= sont utilisés pour créer des actions plus complexes parmi les plus simples:

  • a >> b est une macro-action qui fait l’action a et ensuite l’action b ;
  • a >> a fait l’action a puis l’action a nouveau;
  • avec >>= la deuxième action peut dépendre de la sortie du premier.

Le sens exact de ce qu’est une action et de ce qui fait une action et une autre dépend de la monade: chaque monade définit un sous-langage impératif avec quelques caractéristiques / effets.

Séquençage simple ( >> )

Disons qu’avec une monade donnée M et certaines actions incrementCounter , readCounter , readCounter :

 instance M Monad where ... -- Modify the counter and do not produce any result: incrementCounter :: M () decrementCounter :: M () -- Get the current value of the counter readCounter :: M Integer 

Maintenant, nous aimerions faire quelque chose d’intéressant avec ces actions. La première chose que nous aimerions faire avec ces actions est de les séquencer. Comme dans C, nous aimerions pouvoir faire:

 // This is C: counter++; counter++; 

Nous définissons un “opérateur de séquençage” >> . En utilisant cet opérateur, nous pouvons écrire:

 incrementCounter >> incrementCounter 

Quel est le type de “incrementCounter >> incrementCounter”?

  1. C’est une action faite de deux actions plus petites comme en C, vous pouvez écrire des instructions composées à partir d’instructions atomiques:

     // This is a macro statement made of several statements { counter++; counter++; } // and we can use it anywhere we may use a statement: if (condition) { counter++; counter++; } 
  2. il peut avoir les mêmes effets que ses sous-actions;

  3. il ne produit aucune sortie / résultat.

On aimerait donc que incrementCounter >> incrementCounter soit de type M () : une (macro) action avec le même type d’effets possibles mais sans aucune sortie.

Plus généralement, en deux actions:

 action1 :: M a action2 :: M b 

nous définissons a a >> b comme la macro-action qui est obtenue en faisant (peu importe ce que cela signifie dans notre domaine d’action) a puis b et produit en sortie le résultat de l’exécution de la deuxième action. Le type de >> est:

 (>>) :: M a -> M b -> M b 

ou plus généralement:

 (>>) :: (Monad m) => ma -> mb -> mb 

Nous pouvons définir une plus grande séquence d’actions à partir de plus simples:

  action1 >> action2 >> action3 >> action4 

Entrées et sorties ( >>= )

Nous aimerions pouvoir incrémenter autre chose que 1 à la fois:

 incrementBy 5 

Nous voulons fournir des informations sur nos actions. Pour ce faire, nous définissons une fonction incrementBy un Int et en produisant une action:

 incrementBy :: Int -> M () 

Maintenant, nous pouvons écrire des choses comme:

 incrementCounter >> readCounter >> incrementBy 5 

Mais nous n’avons aucun moyen d’alimenter la sortie de readCounter en incrementBy . Pour ce faire, une version légèrement plus puissante de notre opérateur de séquençage est nécessaire. L’opérateur >>= peut alimenter la sortie d’une action donnée en entrée de l’action suivante. Nous pouvons écrire:

 readCounter >>= incrementBy 

C’est une action qui exécute l’action readCounter , alimente sa sortie dans la fonction incrementBy puis exécute l’action résultante.

Le type de >>= est:

 (>>=) :: Monad m => ma -> (a -> mb) -> mb 

Un exemple (partiel)

Disons que j’ai une monade Prompt qui ne peut afficher que des informations (texte) à l’utilisateur et demander des informations à l’utilisateur:

 -- We don't have access to the internal structure of the Prompt monad module Prompt (Prompt(), echo, prompt) where -- Opaque data Prompt a = ... instance Monad Prompt where ... -- Display a line to the CLI: echo :: Ssortingng -> Prompt () -- Ask a question to the user: prompt :: Ssortingng -> Prompt Ssortingng 

Essayons de définir un promptBoolean message actions qui demande une question et produit une valeur booléenne.

Nous utilisons l’invite (message ++ "[y/n]") et alimentons sa sortie en une fonction f :

  • f "y" devrait être une action qui ne fait que produire True en sortie;

  • f "n" devrait être une action qui ne fait que produire False en sortie;

  • toute autre chose devrait relancer l’action (refaire l’action);

promptBoolean ressemblerait à ceci:

  -- Incomplete version, some bits are missing: promptBoolean :: Ssortingng -> M Boolean promptBoolean message = prompt (message ++ "[y/n]") >>= f where f result = if result == "y" then ???? -- We need here an action which does nothing but produce `True` as output else if result=="n" then ???? -- We need here an action which does nothing but produce `False` as output else echo "Input not recognised, try again." >> promptBoolean 

Produire une valeur sans effet ( return )

Afin de remplir les bits manquants dans notre fonction promptBoolean , nous avons besoin d’un moyen de représenter les actions factices sans aucun effet secondaire, mais qui ne produisent qu’une valeur donnée:

 -- "return 5" is an action which does nothing but outputs 5 return :: (Monad m) => a -> ma 

et nous pouvons maintenant écrire la fonction promptBoolean :

 promptBoolean :: Ssortingng -> Prompt Boolean promptBoolean message :: prompt (message ++ "[y/n]") >>= f where f result = if result=="y" then return True else if result=="n" then return False else echo "Input not recognised, try again." >> promptBoolean message 

En composant ces deux actions simples ( promptBoolean , echo ), nous pouvons définir n’importe quel type de dialog entre l’utilisateur et votre programme (les actions du programme sont déterministes car notre monade n’a pas d’effet aléatoire).

 promptInt :: Ssortingng -> M Int promptInt = ... -- similar -- Classic "guess a number game/dialog" guess :: Int -> m() guess n = promptInt "Guess:" m -> f where fm = if m == n then echo "Found" else (if m > n then echo "Too big" then echo "Too small") >> guess n 

Les opérations d’une monade

Une Monade est un ensemble d’actions pouvant être composé avec les opérateurs return et >>= :

  • >>= pour la composition de l’action;

  • return pour produire une valeur sans effet (latéral).

Ces deux opérateurs sont les opérateurs minimaux nécessaires pour définir une Monad .

Dans Haskell, l’opérateur >> est également nécessaire mais il peut en fait être dérivé de >>= :

 (>>): Monad m => ma -> mb -> mb a >> b = a >>= f where fx = b 

Dans Haskell, un opérateur de fail supplémentaire est également nécessaire, mais il s’agit en fait d’un hack (et il pourrait être supprimé de Monad à l’avenir ).

Voici la définition de Haskell d’une Monad :

 class Monad m where return :: ma (>>=) :: ma -> (a -> mb) -> mb (>>) :: ma -> mb -> mb -- can be derives from (>>=) fail :: Ssortingng -> ma -- mostly a hack 

Les actions sont de première classe

Une bonne chose à propos des monades est que les actions sont de première classe. Vous pouvez les prendre dans une variable, vous pouvez définir une fonction qui prend des actions en entrée et produit d’autres actions en sortie. Par exemple, nous pouvons définir un opérateur while :

 -- while xy : does action y while action x output True while :: (Monad m) => m Boolean -> ma -> m () while xy = x >>= f where f True = y >> while xy f False = return () 

Résumé

Une Monad est un ensemble d’ actions dans certains domaines. Le monad / domain définit le type des “effets” possibles. Les opérateurs >> et >>= représentent le séquençage des actions et l’expression monadique peut être utilisée pour représenter tout type de “sous-programme (impératif)” dans votre programme Haskell (fonctionnel).

Les grandes choses sont que:

  • vous pouvez concevoir votre propre Monad qui prend en charge les fonctionnalités et les effets que vous souhaitez

    • voir Prompt pour un exemple de “sous-programme de dialog seulement”,

    • voir Rand pour un exemple de “sous-programme d’échantillonnage seulement”;

  • Vous pouvez écrire vos propres structures de contrôle ( while , throw , catch ou plus exotiques) en tant que fonctions prenant des actions et les composant de manière à produire des macro-actions plus importantes.

MonadRandom

Le paquet MonadRandom est un bon moyen de comprendre les monades. La monade Rand est composée d’actions dont la sortie peut être aléatoire (l’effet est aléatoire). Une action dans cette monade est une sorte de variable aléatoire (ou plus exactement un processus d’échantillonnage):

  -- Sample an Int from some dissortingbution action :: Rand Int 

L’utilisation de Rand pour faire des échantillonnages / algorithmes aléatoires est très intéressante car vous avez des variables aléatoires comme valeurs de première classe:

 -- Estimate mean by sampling nsamples times the random variable x sampleMean :: Real a => Int -> ma -> ma sampleMean nx = ... 

Dans ce réglage, la fonction de sequence de Prelude ,

  sequence :: Monad m => [ma] -> m [a] 

devient

  sequence :: [Rand a] -> Rand [a] 

Il crée une variable aléatoire obtenue par échantillonnage indépendamment d’une liste de variables aléatoires.

Une chose qui m’aide souvent à comprendre la nature de quelque chose, c’est de l’examiner de la manière la plus sortingviale possible. De cette façon, je ne suis pas distrait par des concepts potentiellement sans rapport. Dans cet esprit, je pense qu’il peut être utile de comprendre la nature de la monade d’identité , car c’est l’implémentation la plus sortingviale d’une monade possible (je pense).

Qu’est-ce qui est intéressant à propos de l’identité Monad? Je pense que cela me permet d’exprimer l’idée d’évaluer des expressions dans un contexte défini par d’autres expressions. Et pour moi, c’est l’essence de chaque Monade que j’ai rencontrée (jusqu’à présent).

Si vous aviez déjà beaucoup de familiarité avec les langages de programmation «traditionnels» avant d’apprendre Haskell (comme je l’ai fait), cela ne semble pas très intéressant du tout. Après tout, dans un langage de programmation classique, les instructions sont exécutées en séquence, l’une après l’autre (à l’exception des constructions à contrôle de stream, bien sûr). Et naturellement, nous pouvons supposer que chaque instruction est évaluée dans le contexte de toutes les instructions précédemment exécutées et que ces instructions précédemment exécutées peuvent modifier l’environnement et le comportement de la déclaration en cours d’exécution.

Tout cela est quasiment un concept étranger dans un langage fonctionnel et paresseux comme Haskell. L’ordre dans lequel les calculs sont évalués dans Haskell est bien défini, mais parfois difficile à prévoir, et encore plus difficile à contrôler. Et pour beaucoup de problèmes, c’est très bien. Mais d’autres types de problèmes (par exemple, les E / S) sont difficiles à résoudre sans un moyen pratique d’établir un ordre et un contexte implicites entre les calculs de votre programme.

En ce qui concerne les effets secondaires, ils peuvent souvent être transformés (via une monade) en un simple passage d’état, ce qui est parfaitement légal dans un langage purement fonctionnel. Certaines monades ne semblent cependant pas être de cette nature. Les monades telles que IO Monad ou ST monad réalisent littéralement des actions secondaires. Il y a plusieurs façons de penser à cela, mais une façon de penser est que, simplement parce que mes calculs doivent exister dans un monde sans effets secondaires, la Monade ne le peut pas. En tant que tel, la Monade est libre d’établir un contexte pour que mes calculs s’exécutent, en fonction des effets secondaires définis par d’autres calculs.

Enfin, je dois nier que je ne suis définitivement pas un expert Haskell. En tant que tel, comprenez s’il vous plaît que tout ce que j’ai dit est à peu près mes propres pensées sur ce sujet et je pourrais très bien les renvoyer plus tard quand je comprendrai plus complètement Monads.

Il y a trois observations principales concernant la monade IO:

1) Vous ne pouvez pas en tirer de valeurs. D’autres types comme Maybe pourraient permettre d’extraire des valeurs, mais ni l’interface de classe monad ni le type de données IO ne le permettent.

2) IO “Inside” n’est pas seulement la valeur réelle mais aussi la chose “RealWorld”. Cette valeur fictive est utilisée pour imposer l’enchaînement des actions par le système de types : Si vous avez deux calculs indépendants, l’utilisation de >>= rend le second calcul dépendant du premier.

3) Supposons une chose non déterministe comme random :: () -> Int , qui n’est pas autorisée dans Haskell. Si vous changez la signature en random :: Blubb -> (Blubb, Int) , il est permis, si vous vous assurez que personne ne pourra jamais utiliser deux fois un Blubb : parce que dans ce cas toutes les entrées sont “différentes”, ce n’est pas un problème que les sorties sont également différentes.

Maintenant, nous pouvons utiliser le fait 1): Personne ne peut obtenir quelque chose de IO , donc nous pouvons utiliser le mannequin RealWord caché dans IO pour servir de Blubb . Il n’ya qu’un seul IO dans l’ensemble de l’application (celui que l’on obtient du main ), et il s’agit de la bonne séquençage, comme nous l’avons vu dans 2). Problème résolu.

le point est donc qu’il peut y avoir une gestion des erreurs propre dans une chaîne de fonctions, de conteneurs et d’effets secondaires

Plus ou moins.

Comment le problème des effets secondaires est-il résolu?

Une valeur dans la monade d’E / S, c’est-à-dire une de type IO a , doit être interprétée comme un programme. p >> q sur les valeurs IO peut alors être interprété comme l’opérateur qui combine deux programmes en un qui exécute d’abord p , puis q . Les autres opérateurs de monade ont des interprétations similaires. En affectant un programme au nom main , vous déclarez au compilateur qu’il s’agit du programme qui doit être exécuté par son code d’object de sortie.

Quant à la monade de liste, elle n’est pas vraiment liée à la monade d’E / S, sauf dans un sens mathématique très abstrait. La monade IO donne un calcul déterministe avec effets secondaires, tandis que la monade de liste donne une recherche non déterministe (mais pas aléatoire!), Quelque peu similaire au mode opératoire de Prolog.

Avec ce concept de conteneurs, le langage dit essentiellement que tout ce qui est à l’intérieur des conteneurs est non déterministe.

Haskell est déterministe. Si vous demandez un ajout entier 2 + 2, vous obtiendrez toujours 4.

“Non déterministe” n’est qu’une métaphore, une façon de penser. Tout est déterministe sous le capot. Si vous avez ce code:

 do x <- [4,5] y <- [0,1] return (x+y) 

il est à peu près équivalent au code Python

  l = [] for x in [4,5]: for y in [0,1]: l.append(x+y) 

Vous voyez le non-déterminisme ici? Non, c'est une construction déterministe d'une liste. Lancez-le deux fois, vous obtiendrez les mêmes numéros dans le même ordre.

Vous pouvez le décrire de la manière suivante: Choisissez un x arbitraire parmi [4,5]. Choisissez y arbitraire y de [0,1]. Retourne x + y. Recueillez tous les résultats possibles.

Cela semble impliquer le non-déterminisme, mais ce n’est qu’une boucle nestede (compréhension de la liste). Il n'y a pas de "vrai" déterminisme ici, il est simulé en vérifiant toutes les possibilités. Le non-déterminisme est une illusion. Le code semble seulement être non déterministe.

Ce code utilisant l'état monad:

 do put 0 x <- get put (x+2) y <- get return (y+3) 

donne 5 et semble impliquer un changement d'état. Comme avec les listes, c'est une illusion. Il n'y a pas de "variables" qui changent (comme dans les langages impératifs). Tout est imputable sous le capot.

Vous pouvez décrire le code de cette façon: mettez 0 dans une variable. Lire la valeur d'une variable à x. Mettez (x + 2) à la variable. Lisez la variable à y et retournez y + 3.

Cela semble impliquer l'état, mais ce ne sont que des fonctions de composition qui passent un paramètre supplémentaire. Il n'y a pas de "vraie" mutabilité ici, elle est simulée par la composition. La mutabilité est une illusion. Le code semble seulement l'utiliser.

Haskell le fait de cette façon: vous avez des fonctions

  a -> s -> (b,s) 

Cette fonction prend et ancienne valeur d'état et renvoie une nouvelle valeur. Cela n'implique pas de mutabilité ou de changement de variables. C'est une fonction au sens mathématique.

Par exemple, la fonction "put" prend une nouvelle valeur d'état, ignore l'état actuel et renvoie un nouvel état:

  put x _ = ((), x) 

Tout comme vous pouvez composer deux fonctions normales

  a -> b b -> c 

dans

  a -> c 

en utilisant l'opérateur (.), vous pouvez composer des transformateurs "d'état"

  a -> s -> (b,s) b -> s -> (c,s) 

en une seule fonction

  a -> s -> (c,s) 

Essayez d’écrire vous-même l’opérateur de composition. C'est ce qui se passe vraiment, il n'y a pas d'effets secondaires qui ne font que transmettre des arguments aux fonctions.