48 votes

Pourquoi les monades ? Comment résout-il les effets de bord ?

J'apprends Haskell et j'essaie de comprendre les Monads. J'ai deux questions :

  1. D'après ce que j'ai compris, Monad est juste une autre classe de type qui déclare des façons d'interagir avec les données à l'intérieur de "conteneurs", notamment Maybe , List y IO . Il semble intelligent et propre d'implémenter ces 3 choses avec un seul concept, mais en réalité, le but est de permettre une gestion propre des erreurs dans une chaîne de fonctions, de conteneurs et d'effets secondaires. Est-ce une interprétation correcte ?

  2. Comment le problème des effets secondaires est-il résolu ? Avec ce concept de conteneurs, le langage dit essentiellement que tout ce qui se trouve à l'intérieur des conteneurs est non-déterministe (comme les entrées/sorties). Parce que les listes et les E/S sont toutes deux des conteneurs, les listes sont classées par équivalence avec les E/S, même si les valeurs contenues dans les listes me semblent plutôt déterministes. Alors, qu'est-ce qui est déterministe et qu'est-ce qui a des effets secondaires ? Je n'arrive pas à me faire à l'idée qu'une valeur de base est déterministe, jusqu'à ce qu'on la mette dans un conteneur (qui n'est rien de plus que la même valeur avec d'autres valeurs à côté, par ex. Nothing ) et il peut maintenant être aléatoire.

Quelqu'un peut-il expliquer comment, intuitivement, Haskell s'en sort en changeant l'état avec les entrées et les sorties ? Je ne vois pas la magie ici.

0 votes

Cette série pourrait vous être utile, elle parle de CPS mais je pense que les aspects fonctionnels s'appliquent également ici. blogs.msdn.com/b/ericlippert/archive/tags/

3 votes

Vous supposez beaucoup de choses sur ce que les monades sont censées être. Mais elles sont bien plus générales que cela, et à peu près tout ce que vous citez n'est qu'une application spécifique du concept général. Toutes les monades ne sont pas non déterministes, toutes les monades ne tournent pas autour des effets secondaires, etc. Lire haskell.org/haskellwiki/What_a_Monad_is_not

1 votes

Cela ne sert à rien de savoir ce que ce n'est pas. En fait, je n'ai pas vu beaucoup d'exemples de Monads au-delà de IO et Maybe. C'est bien que ceux-ci aient été implémentés avec des fonctionnalités du langage, mais quelles sont ses autres utilisations au-delà de IO ? Si un livre consacre la moitié de ses pages aux Monads, il doit bien y avoir quelque chose d'utile à leur sujet.

38voto

mergeconflict Points 4233

Le but est d'assurer une gestion propre des erreurs dans une chaîne de fonctions, de conteneurs et d'effets secondaires. Est-ce une interprétation correcte ?

Pas vraiment. Vous avez mentionné de nombreux concepts que les gens citent lorsqu'ils tentent d'expliquer les monades, notamment les effets secondaires, la gestion des erreurs et le non-déterminisme, mais il semble que vous ayez le sentiment erroné que tous ces concepts s'appliquent à toutes les monades. Mais il y a un concept que vous avez mentionné qui s'applique : enchaînement .

Il existe deux variantes de ce phénomène, c'est pourquoi je vais l'expliquer de deux manières différentes : une sans effets secondaires, et une avec effets secondaires.

Aucun effet secondaire :

Prenons l'exemple suivant :

addM :: (Monad m, Num a) => m a -> m a -> m a
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 certaine monade. Quelle monade ? Aucune importance ! Dans tous les cas, la monade spéciale do la syntaxe se désagrège en ce qui concerne les éléments suivants :

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

... ou, avec une préséance des opérateurs explicite :

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

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

Ok, voyons quelques exemples de comment addM se comporte à travers différentes monades. Le site Identity est un bon point de départ, puisque sa définition est triviale :

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

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

addM (Identity 1) (Identity 2)

Développant ceci, é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

Super. Maintenant, puisque vous avez parlé de la gestion propre des erreurs, regardons la Maybe monade. Sa définition n'est que légèrement plus délicate que Identity :

instance Monad Maybe where
    return a = Just a  -- same as Identity monad!
    (Just a) >>= f = f a  -- 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 rire, élargissons addM Nothing (Just 1) à la 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

Ainsi, le Maybe la définition de la monade >>= a été modifié pour tenir compte de l'échec. Lorsqu'une fonction est appliquée à un Maybe valeur en utilisant >>= vous obtenez ce à quoi vous vous attendez.

Ok, donc vous avez mentionné le non-déterminisme. Oui, la monade liste peut être considérée comme un modèle de non-déterminisme dans un sens... C'est un peu bizarre, mais pensez à la liste comme représentant des valeurs alternatives possibles : [1, 2, 3] n'est pas une collection, c'est un seul nombre non déterministe qui peut être soit un, deux ou trois. Cela semble stupide, mais cela commence à avoir du sens quand vous pensez à la façon dont >>= est définie pour les listes : elle applique la fonction donnée à chaque valeur possible. Ainsi, addM [1, 2] [3, 4] va en fait calculer toutes les sommes 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 demandez addM à deux valeurs dans le IO monade, comme :

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

Vous n'obtenez rien de spécial, juste 3 dans les IO monade. addM ne lit ni n'écrit aucun état mutable, donc ce n'est pas très amusant. Il en va de même pour le State o ST monades. Ce n'est pas drôle. Utilisons donc une fonction différente :

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

Il est clair que le monde sera différent à chaque fois que des missiles seront tirés. Clairement. Imaginons maintenant que vous essayez d'écrire un code totalement inoffensif, sans effet secondaire et sans tir de missiles. Vous essayez peut-être une fois de plus d'additionner deux nombres, mais cette fois sans qu'aucune monade ne vienne s'y mêler :

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

et tout d'un coup votre main glisse, et vous faites une faute de frappe accidentelle :

add a b = a + b + fireTheMissiles

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

D'accord, c'est un exemple totalement artificiel, mais le fait est que dans le cas de IO , ST et amis, le système de types maintient les effets isolés dans un contexte spécifique. Il n'élimine pas magiquement les effets secondaires, rendant le code transparent par rapport aux références qui ne devraient pas l'être, mais il indique clairement au moment de la compilation à quelle portée les effets sont limités.

Pour en revenir au point initial, quel est le rapport avec le chaînage ou la composition de fonctions ? Eh bien, dans ce cas, il s'agit simplement d'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 certaine politique de chaînage des calculs. Identity La politique de l'UE est une pure composition de fonctions, Maybe La politique de l'entreprise est une composition de fonctions avec propagation de l'échec, IO La politique de l'UE est la suivante impur la composition des fonctions, etc.

5 votes

main = forever fireTheMissiles considéré comme nuisible.

1 votes

@PyRulez Ce programme est atrocement mauvais. Il n'utilise même pas la concurrence ! A beaucoup La meilleure version est main = forever $ forkIO fireTheMissiles

11voto

Peter Wortmann Points 1861

Permettez-moi de commencer par citer l'excellent " Vous auriez pu inventer les monades L'article "Monad". Il illustre comment la structure Monad peut se manifester naturellement pendant que vous écrivez des programmes. Mais le tutoriel ne mentionne pas IO Je vais donc tenter ici d'étendre cette approche.

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

f, g :: Int -> [Int]

Une façon de voir les choses est de dire que cela nous donne un certain nombre de sorties possibles pour chaque entrée possible. Et si nous voulons toutes les sorties possibles pour le composition des deux fonctions ? En donnant toutes les possibilités que nous pourrions obtenir en appliquant les fonctions l'une après l'autre ?

Il y a une fonction pour ça :

fg x = concatMap g $ f x

Si nous généralisons, nous obtenons

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

Pourquoi voudrions-nous l'envelopper de cette façon ? Eh bien, écrire nos programmes en utilisant principalement >>= y return nous donne quelques propriétés intéressantes - par exemple, nous pouvons être sûrs qu'il est relativement difficile d'"oublier" des solutions. Nous devrions explicitement la réintroduire, par exemple en ajoutant une autre fonction skip . De plus, nous avons maintenant une monade et pouvons utiliser tous les combinateurs de la bibliothèque des monades !

Maintenant, passons à votre exemple plus délicat. Disons que les deux fonctions ont 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 (puisqu'il peut les influencer) et leur sortie (puisque la fonction peut l'influencer). On obtient donc quelque chose comme :

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

Si nous voulons maintenant f pour obtenir le monde que g laissés derrière, nous écrivions :

fg x rw = let (y, rw')  = f x rw
              (r, rw'') = g y rw'
           in (r, rw'')

Ou généralisé :

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

Maintenant, si l'utilisateur peut seulement utiliser >>= , return et quelques prédéfinis IO les valeurs qui nous permettent de retrouver une belle propriété : L'utilisateur n'aura jamais voir le site RealWorld# qui circule ! Et c'est une très bonne chose, car vous n'êtes pas vraiment intéressé par les détails de l'endroit où getLine reçoit ses données. Et à nouveau nous obtenons toutes les belles fonctions de haut niveau des bibliothèques de monades.

Donc les choses importantes à retenir :

  1. La monade capture les modèles communs dans votre code, comme "toujours passer tous les éléments du conteneur A au conteneur B" ou "passer cette étiquette du monde réel". Souvent, une fois que vous avez compris 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 complètement cacher l'implémentation à l'utilisateur. Il s'agit d'un excellent mécanisme d'encapsulation, que ce soit pour votre propre état interne ou pour la façon dont IO parvient à faire entrer la non-pureté dans un programme pur d'une manière relativement sûre.


Annexe

Au cas où quelqu'un se gratte encore la tête sur RealWorld# autant que lorsque j'ai commencé : Il y a évidemment plus de magie qui se passe après toute l'abstraction de la monade a é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 qui est ce que nous recherchions !)

  2. Mais cela signifie aussi qu'il n'est pas nécessaire de passer par le monde réel car il n'y a qu'un seul monde possible : Celui qui est courant lorsque la fonction est exécutée !

L'essentiel est qu'une fois l'ordre d'exécution fixé, RealWorld# est simplement optimisé. Par conséquent, les programmes utilisant le IO n'ont en fait aucune incidence sur le temps d'exécution. Notez également que l'utilisation de RealWorld# est évidemment seulement un manière possible de mettre IO - mais il se trouve que c'est celui que GHC utilise en interne. L'avantage des monades est que, là encore, l'utilisateur n'a pas besoin de le savoir.

0 votes

Excellente explication. Merci, Peter ! Pour résumer avec mes propres mots, juste pour le plaisir, les monades permettent de faire circuler un contexte caché que les appelants n'ont pas à voir ou à utiliser. Les effets secondaires sont obtenus en faisant circuler un contexte "monde réel" généré par le compilateur qui est modifié (entrée/sortie IO) à chaque succession d'un appel IO.

0 votes

Oui - ces contextes sont à la fois l'une des applications les plus faciles et les plus utiles à connaître. Mais pour être juste, il y a beaucoup d'autres "patterns" qui peuvent être capturés et qui n'ont pas grand chose à voir avec le passage de contextes - par exemple la monade d'exception encapsulant le "do". x et seulement s'il n'échoue pas, continuez avec y "en faisant Maybe a une monade. La traduction est en fait assez simple - je vous encourage à l'essayer vous-même ou à la rechercher.

11voto

ysdx Points 2444

Vous pourriez voir une monade donnée m comme un ensemble/famille (ou royaume, domaine, etc.) de actions (pensez à une déclaration C). La monade m définit le type d'effets (secondaires) que ses actions peuvent avoir :

  • avec [] vous pouvez définir des actions qui peuvent bifurquer 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 qui peuvent avoir des effets secondaires sur le "monde extérieur" (accéder à des fichiers, au réseau, lancer des processus, faire un GET HTTP ...) ;
  • vous pouvez disposer d'une monade dont l'effet est le "hasard" (voir le package MonadRandom ) ;
  • vous pouvez définir une monade dont les actions peuvent effectuer un déplacement dans un jeu (disons les échecs, le go ) et recevoir un déplacement d'un adversaire, mais qui n'est pas capable d'écrire dans votre système de fichiers ou autre.

Résumé

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

El >> y >>= Les opérateurs sont utilisés pour créer des actions plus complexes à partir d'actions plus simples :

  • a >> b est une macro-action qui fait l'action a et ensuite l'action b ;
  • a >> a fait l'action a et ensuite l'action a encore ;
  • avec >>= la deuxième action peut dépendre de la sortie de la première.

La signification exacte de ce qu'est un action est et ce que faire une action et ensuite une autre dépend de la monade : chaque monade définit un sous-langage impératif avec certaines caractéristiques/effets.

Séquençage simple ( >> )

Disons que nous avons une monade donnée M et quelques actions incrementCounter , decrementCounter , 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

Nous aimerions maintenant faire quelque chose d'intéressant avec ces actions. La première chose que nous voulons faire avec ces actions est de les séquencer. Comme en C, par exemple, 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 composée de deux actions plus petites, comme en C, vous pouvez écrire des déclarations composées à partir de déclarations 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 le même type d'effets que ses sous-actions ;

  3. il ne produit pas de sortie/résultat.

Nous aimerions donc incrementCounter >> incrementCounter pour être de type M () une (macro-)action avec le même type d'effets possibles mais sans aucun résultat.

Plus généralement, étant donné deux actions :

action1 :: M a
action2 :: M b

nous définissons une a >> b comme la macro-action qui est obtenue par en faisant (quoi que cela signifie dans notre domaine d'action) a puis b et produit en sortie le résultat de l'exécution de la seconde action. Le type de >> est :

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

ou plus généralement :

(>>) :: (Monad m) => m a -> m b -> m b

Nous pouvons définir des séquences d'actions plus importantes à partir d'actions plus simples :

 action1 >> action2 >> action3 >> action4

Entrées et sorties ( >>= )

Nous aimerions être en mesure d'incrémenter par autre chose que 1 à la fois :

incrementBy 5

Nous voulons fournir une entrée dans nos actions, pour ce faire, nous définissons une fonction incrementBy prendre un Int et de produire 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. Le site >>= L'opérateur peut utiliser la sortie d'une action donnée comme entrée de l'action suivante. On peut écrire :

readCounter >>= incrementBy

Il s'agit d'une action qui exécute le readCounter et alimente sa sortie dans le incrementBy et ensuite exécuter l'action qui en résulte.

Le type de >>= est :

(>>=) :: Monad m => m a -> (a -> m b) -> m b

Un exemple (partiel)

Disons que j'ai un Prompt monade qui ne peut qu'afficher 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 :: String -> Prompt ()

-- Ask a question to the user:
prompt :: String -> Prompt String

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

Nous utilisons l'invite (message ++ "[y/n]") et alimenter sa sortie à une fonction f :

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

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

  • toute autre action devrait redémarrer l'action (refaire l'action) ;

promptBoolean ressemblerait à ceci :

    -- Incomplete version, some bits are missing:
    promptBoolean :: String -> 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 combler les lacunes de notre promptBoolean nous avons besoin d'un moyen de représenter des actions fictives sans 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 -> m a

et nous pouvons maintenant écrire promptBoolean función:

promptBoolean :: String -> 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 dialogue entre l'utilisateur et votre programme (les actions du programme sont déterministes car notre monade n'a pas d'"effet aléatoire").

promptInt :: String -> M Int
promptInt = ... -- similar

-- Classic "guess a number game/dialogue"
guess :: Int -> m()
guess n = promptInt "Guess:" m -> f
   where f m = 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 qui peuvent être composées avec l'attribut return y >>= opérateurs :

  • >>= pour la composition des actions ;

  • return pour produire une valeur sans aucun effet (secondaire).

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

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

(>>): Monad m => m a -> m b -> m b
a >> b = a >>= f
 where f x = b

En Haskell, un supplément fail est également nécessaire, mais il s'agit vraiment d'un hack (et de il pourrait être retiré de Monad à l'avenir ).

C'est la définition Haskell d'un Monad :

class Monad m where     
  return :: m a     
  (>>=) :: m a -> (a -> m b) -> m b     
  (>>) :: m a -> m b -> m b  -- can be derived from (>>=)
  fail :: String -> m a      -- mostly a hack

Les actions sont de premier ordre

L'un des avantages des monades est que les actions sont de première classe. Vous pouvez les prendre dans une variable, vous pouvez définir des fonctions qui prennent des actions en entrée et produisent d'autres actions en sortie. Par exemple, nous pouvons définir une while opérateur :

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

Résumé

A Monad est un ensemble de actions dans un certain domaine. La monade/le domaine définissent le type d'"effets" qui sont possibles. Le site >> y >>= Les opérateurs 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 supporte les fonctionnalités et les effets que vous voulez

    • voir Prompt pour un exemple de "sous-programme de dialogue uniquement",

    • voir Rand pour un exemple de "sous-programme d'échantillonnage uniquement" ;

  • vous pouvez écrire vos propres structures de contrôle ( while , throw , catch ou plus exotiques) comme des fonctions prenant des actions et les composant d'une certaine manière pour produire une macro-action plus importante.

MonadRandom

Un bon moyen de comprendre les monades est l'outil d'analyse de l'environnement. MonadRandom paquet. Le site Rand La monade est constituée d'actions dont la sortie peut être aléatoire (l'effet est le caractère aléatoire). Un site action dans cette monade est une sorte de variable aléatoire (ou plus exactement un processus d'échantillonnage) :

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

Utilisation de Rand pour faire de l'échantillonnage/des algorithmes aléatoires est assez intéressant car vous avez variables aléatoires comme des valeurs de premier ordre :

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

Dans ce contexte, le sequence de la fonction Prelude ,

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

devient

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

Il crée une variable aléatoire obtenue par échantillonnage indépendant à partir d'une liste de variables aléatoires.

5voto

Landei Points 30509

Il existe trois observations principales concernant la monade IO :

1) Vous ne pouvez pas en tirer de valeurs. D'autres types comme Maybe pourrait permettre d'extraire des valeurs, mais ni l'interface de la classe monade elle-même, ni la classe IO type de données le permettent.

2) "Inside" IO n'est pas seulement la valeur réelle mais aussi cette chose du "monde réel". Cette valeur fictive est utilisée pour renforcer l'enchaînement des actions. par le système de type : Si vous avez deux calculs indépendants, l'utilisation de la fonction >>= rend le second calcul dépendant du premier.

3) Supposons une chose non déterministe comme random :: () -> Int ce qui n'est pas autorisé en Haskell. Si vous changez la signature en random :: Blubb -> (Blubb, Int) il es autorisé, si vous vous assurez que personne ne puisse jamais utiliser une Blubb deux fois : parce que dans ce cas, toutes les entrées sont "différentes", il n'y a aucun problème à ce que les sorties soient également différentes.

Maintenant, nous pouvons utiliser le fait 1) : Personne ne peut obtenir quelque chose de IO Nous pouvons donc utiliser le RealWord mannequin caché dans IO pour servir de Blubb . Il n'y a qu'un seul IO dans l'ensemble de l'application (celui que nous obtenons à partir de main ), et il s'occupe de la séquentiation correcte, comme nous l'avons vu en 2). Problème résolu.

4voto

Daniel Pratt Points 8151

Une chose qui m'aide souvent à comprendre la nature de quelque chose est de l'examiner de la manière la plus triviale possible. De cette façon, je ne me laisse pas distraire par des concepts potentiellement sans rapport. En gardant cela à l'esprit, je pense qu'il peut être utile de comprendre la nature des Monade d'identité car c'est l'implémentation la plus triviale possible d'un Monad (je pense).

Qu'est-ce qui est intéressant dans la monade d'identité ? Je pense que c'est qu'elle 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 tous les monades que j'ai rencontrés (jusqu'à présent).

Si vous avez déjà été exposé à des langages de programmation "classiques" avant d'apprendre Haskell (comme moi), cela ne semble pas du tout intéressant. 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 de flux de contrôle, bien sûr). Et naturellement, nous pouvons supposer que chaque instruction est évaluée dans le contexte de toutes les instructions exécutées précédemment et que ces instructions exécutées précédemment peuvent modifier l'environnement et le comportement de l'instruction en cours d'exécution.

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

En ce qui concerne les effets secondaires, spécifiquement, ils peuvent souvent être transformés (via une Monade) en un simple passage d'état, ce qui est parfaitement légal dans un langage fonctionnel pur. Certaines monades ne semblent toutefois pas être de cette nature. Des monades telles que la monade IO ou la monade ST effectuent littéralement des actions à effet secondaire. Il y a de nombreuses façons de penser à cela, mais l'une d'entre elles est que, si mes calculs doivent exister dans un monde sans effets secondaires, ce n'est pas le cas de la monade. En tant que telle, la monade est libre d'établir un contexte pour l'exécution de mon calcul qui est basé sur des effets secondaires définis par d'autres calculs.

Enfin, je dois préciser que je ne suis absolument pas un expert en Haskell. En tant que tel, comprenez que tout ce que j'ai dit est à peu près mes propres pensées sur le sujet et que je pourrais très bien les renier plus tard, lorsque je comprendrai mieux les Monads.

0 votes

La monade la plus triviale est probablement la monade crapy : data Crappy a = Crappy. Mais elle n'est pas très utile.

0 votes

J'aime comment cette réponse complète celle de Peter. Laissez-moi savoir si je comprends bien cette perspective : au lieu que notre programme modifie le monde en appelant une API spéciale (par exemple <stdio.h> en C), il s'agit plutôt de définir des fonctions sans effets secondaires, et de les faire appeler par un ensemble d'API (monades IO) qui transmettent à ces fonctions différentes choses en fonction de l'entrée utilisateur/fichier/réseau. (Cela rend tout ce que nous programmons en Haskell déterministe, car le langage lui-même nous restreint de définir quoi que ce soit qui modifie l'IO).

0 votes

@MTsoul Oui, je pense que vous comprenez ce point de vue au moins aussi bien que moi :). Je trouve que la perspective du #RealWorld state-passing est à la fois intéressante et instructive, mais il semble que de nombreux Haskellers purs et durs n'aiment pas cette explication parce qu'elle se concentre sur (ce qui revient à) un détail de mise en œuvre. Par ailleurs, je trouve que l'explication donnée par ysdx est une autre bonne perspective car, à l'intérieur d'un calcul pur, les effets secondaires sont modélisés comme des valeurs décrivant des actions à réaliser (par opposition à la réalisation effective de l'action).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X