Les avantages du modèle de monade IO pour la gestion des effets secondaires sont-ils purement académiques?

17

Désolé pour encore une autre question d'effets secondaires FP +, mais je n'ai pas pu trouver une question existante qui répondait tout à fait à moi.

Ma compréhension (limitée) de la programmation fonctionnelle est que les effets d'état / secondaires doivent être minimisés et séparés de la logique sans état.

Je rassemble également l'approche de Haskell à ce sujet, la monade IO, atteint cet objectif en enveloppant les actions avec état dans un conteneur, pour une exécution ultérieure, considérée en dehors de la portée du programme lui-même.

J'essaie de comprendre ce modèle, mais en fait de déterminer s'il faut l'utiliser dans un projet Python, donc je veux éviter les spécificités de Haskell si possible.

Exemple brut entrant.

Si mon programme convertit un fichier XML en fichier JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

L'approche de la monade IO n'est-elle pas efficace pour ce faire:

steps = list(
    read_file,
    convert,
    write_file,
)

alors se dégager de sa responsabilité en n'appelant pas réellement ces étapes, mais en laissant l'interprète le faire?

Autrement dit, c'est comme écrire:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

puis attendre que quelqu'un d'autre appelle inner()et dire que votre travail est fait parce que main()c'est pur.

Tout le programme va finir par être contenu dans la monade IO, essentiellement.

Lorsque le code est réellement exécuté , tout après la lecture du fichier dépend de l'état de ce fichier, il souffrira donc toujours des mêmes bogues liés à l'état que l'implémentation impérative, alors avez-vous réellement gagné quelque chose, en tant que programmeur qui le maintiendra?

J'apprécie totalement l'avantage de réduire et d' isoler les comportements avec état, c'est pourquoi j'ai structuré la version impérative comme ça: rassembler des entrées, faire des trucs purs, cracher des sorties. Espérons que cela convert()peut être complètement pur et profiter des avantages de la cachabilité, de la sécurité des fils, etc.

J'apprécie également que les types monadiques puissent être utiles, en particulier dans les pipelines fonctionnant sur des types comparables, mais je ne vois pas pourquoi les entrées-sorties devraient utiliser des monades à moins qu'elles ne soient déjà dans un tel pipeline.

Y a-t-il un avantage supplémentaire à gérer les effets secondaires que le modèle de monade IO apporte, ce qui me manque?

Stu Cox
la source
1
Vous devriez regarder cette vidéo . Les merveilles des monades sont enfin révélées sans recourir à la théorie des catégories ou à Haskell. Il s'avère que les monades sont trivialement exprimées en JavaScript et sont l'un des principaux catalyseurs d'Ajax. Les monades sont incroyables. Ce sont des choses simples, presque trivialement mises en œuvre, avec un pouvoir énorme pour gérer la complexité. Mais les comprendre est étonnamment difficile, et la plupart des gens, une fois qu'ils ont ce moment ah-ha, semblent perdre la capacité de les expliquer aux autres.
Robert Harvey
Bonne vidéo, merci. J'ai en fait appris ce truc d'une intro JS à la programmation fonctionnelle (puis lu un million de plus…). Bien qu'ayant regardé cela, je suis presque sûr que ma question est spécifique à la monade IO, que Crock ne couvre pas dans cette vidéo.
Stu Cox
Hmm ... AJAX n'est-il pas considéré comme une forme d'E / S?
Robert Harvey
1
Notez que le type d' mainun programme Haskell est IO ()- une action d'E / S. Ce n'est pas du tout une fonction; c'est une valeur . L'ensemble de votre programme est une valeur pure contenant des instructions qui indiquent au runtime de langue ce qu'il doit faire. Tous les trucs impurs (en fait les actions d'E / S) sortent du cadre de votre programme.
Wyzard --stop Maltraiter Monica--
Dans votre exemple, la partie monadique consiste à prendre le résultat d'un calcul ( read_file) et à l'utiliser comme argument pour le suivant ( write_file). Si vous n'aviez qu'une séquence d'actions indépendantes, vous n'auriez pas besoin d'une Monade.
lortabac

Réponses:

14

Tout le programme va finir par être contenu dans la monade IO, essentiellement.

C'est le peu où je pense que vous ne voyez pas cela du point de vue des Haskellers. Nous avons donc un programme comme celui-ci:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Je pense qu'un point de vue typique de Haskeller sur ce serait que convert, la partie pure:

  1. Est probablement la majeure partie de ce programme, et de loin plus compliquée que les IOparties;
  2. Peut être raisonné et testé sans avoir à le faire IOdu tout.

Ils ne voient donc pas cela comme convertétant "contenu" dans IO, mais plutôt comme étant isolé de IO. De son type, quoi convertque ce soit ne peut jamais dépendre de tout ce qui se passe dans une IOaction.

Lorsque le code est réellement exécuté, tout après la lecture du fichier dépend de l'état de ce fichier, il souffrira donc toujours des mêmes bogues liés à l'état que l'implémentation impérative, alors avez-vous réellement gagné quelque chose, en tant que programmeur qui le maintiendra?

Je dirais que cela se divise en deux choses:

  1. Lorsque le programme s'exécute, la valeur de l' argument à convertdépend de l'état du fichier.
  2. Mais ce que la convertfonction fait , cela ne dépend pas de l'état du fichier. convertest toujours la même fonction , même si elle est invoquée avec différents arguments à différents points.

C'est un point quelque peu abstrait, mais c'est vraiment la clé de ce que veulent dire Haskellers lorsqu'ils en parlent. Vous voulez écrire convertde telle manière que, étant donné tout argument valide, il produira un résultat correct pour cet argument. Quand vous le regardez comme ça, le fait que la lecture d'un fichier soit une opération avec état n'entre pas dans l'équation; tout ce qui compte, c'est que tout argument qui lui est fourni et d'où qu'il provienne, convertdoit le gérer correctement. Et le fait que la pureté limite ce qui convertpeut être fait avec son entrée simplifie ce raisonnement.

Donc, si convertproduit des résultats incorrects à partir de certains arguments, et lui readFiledonne un tel argument, nous ne voyons pas cela comme un bogue introduit par l' état . C'est un bug dans une fonction pure!

sacundim
la source
Je pense que c'est la meilleure description (même si les autres ont aidé à clarifier les choses pour moi aussi), merci.
Stu Cox
vaut-il la peine de noter que l'utilisation de monades en python peut avoir moins d'avantages car python n'a qu'un seul type (statique), et ne fera donc aucune garantie sur quoi que ce soit?
jk.
7

Il est difficile de savoir exactement ce que vous entendez par «purement académique», mais je pense que la réponse est principalement «non».

Comme expliqué dans Tackling the Awkward Squad par Simon Peyton Jones ( lecture fortement recommandée!), Les E / S monadiques étaient censées résoudre de vrais problèmes avec la façon dont Haskell gérait les E / S. Lisez l'exemple du serveur avec Demandes et réponses, que je ne copierai pas ici; c'est très instructif.

Haskell, contrairement à Python, encourage un style de calcul "pur" qui peut être appliqué par son système de types. Bien sûr, vous pouvez utiliser l'autodiscipline lors de la programmation en Python pour vous conformer à ce style, mais qu'en est-il des modules que vous n'avez pas écrits? Sans beaucoup d'aide du système de type (et des bibliothèques communes), les E / S monadiques sont probablement moins utiles en Python. La philosophie de la langue n'est tout simplement pas destinée à imposer une stricte séparation pure / impure.

Notez que cela en dit plus sur les différentes philosophies de Haskell et Python que sur la façon dont les E / S monadiques académiques sont. Je ne l'utiliserais pas pour Python.

Une autre chose. Vous dites:

Tout le programme va finir par être contenu dans la monade IO, essentiellement.

Il est vrai que la mainfonction Haskell "vit" IO, mais les vrais programmes Haskell sont encouragés à ne pas l'utiliser IOchaque fois que cela n'est pas nécessaire. Presque toutes les fonctions que vous écrivez qui n'ont pas besoin de faire d'E / S ne devraient pas avoir de type IO.

Je dirais donc que dans votre dernier exemple, vous l'avez fait à l'envers: mainest impur (car il lit et écrit des fichiers) mais les fonctions de base comme convertsont pures.

Andres F.
la source
3

Pourquoi IO est-il impur? Parce qu'il peut renvoyer différentes valeurs à différents moments. Il y a une dépendance au temps qui doit être prise en compte, d'une manière ou d'une autre. Ceci est encore plus crucial avec une évaluation paresseuse. Considérez le programme suivant:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Sans une monade d'E / S, pourquoi la première invite obtiendrait-elle une sortie? Il n'y a rien en fonction de cela, donc une évaluation paresseuse signifie qu'elle ne sera jamais demandée. Il n'y a également rien qui oblige l'invite à sortir avant la lecture de l'entrée. En ce qui concerne l'ordinateur, sans monade d'E / S, ces deux premières expressions sont complètement indépendantes l'une de l'autre. Heureusement, nameimpose un ordre aux deux seconds.

Il existe d'autres façons de résoudre le problème de la dépendance à l'ordre, mais l'utilisation d'une monade IO est probablement le moyen le plus simple (du moins du point de vue du langage) pour permettre à tout de rester dans le domaine fonctionnel paresseux, sans petites sections de code impératif. C'est aussi le plus flexible. Par exemple, vous pouvez relativement facilement créer dynamiquement un pipeline d'E / S lors de l'exécution en fonction des entrées de l'utilisateur.

Karl Bielefeldt
la source
2

Ma compréhension (limitée) de la programmation fonctionnelle est que les effets d'état / secondaires doivent être minimisés et séparés de la logique sans état.

Ce n'est pas seulement une programmation fonctionnelle; c'est généralement une bonne idée dans n'importe quelle langue. Si vous faites des tests unitaires, la façon dont vous vous séparez read_file(), convert()et write_file()vient parfaitement naturellement parce que, bien qu'il convert()soit de loin la partie la plus complexe et la plus importante du code, l'écriture de tests est relativement facile: il vous suffit de configurer le paramètre d'entrée . L'écriture de tests pour read_file()et write_file()est un peu plus difficile (même si les fonctions elles-mêmes sont presque triviales) car vous devez créer et / ou lire des choses sur le système de fichiers avant et après l'appel de la fonction. Idéalement, vous rendriez ces fonctions si simples que vous vous sentiriez à l'aise de ne pas les tester et ainsi vous évitez beaucoup de tracas.

La différence entre Python et Haskell ici est que Haskell a un vérificateur de type qui peut prouver que les fonctions n'ont pas d'effets secondaires. En Python, vous devez espérer que personne n'a accidentellement laissé tomber une fonction de lecture ou d'écriture de fichiers dans convert()(disons read_config_file()). Dans Haskell lorsque vous déclarez convert :: String -> Stringou similaire, sans IOmonade, le vérificateur de type garantira qu'il s'agit d'une fonction pure qui ne dépend que de son paramètre d'entrée et de rien d'autre. Si quelqu'un essaie de modifier convertpour lire un fichier de configuration, il verra rapidement les erreurs du compilateur montrant qu'il briserait la pureté de la fonction. (Et j'espère qu'ils seraient assez raisonnables pour read_config_filesortir convertet transmettre son résultat convert, en maintenant la pureté.)

Curt J. Sampson
la source