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?
main
un programme Haskell estIO ()
- 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.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.Réponses:
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:
Je pense qu'un point de vue typique de Haskeller sur ce serait que
convert
, la partie pure:IO
parties;IO
du tout.Ils ne voient donc pas cela comme
convert
étant "contenu" dansIO
, mais plutôt comme étant isolé deIO
. De son type, quoiconvert
que ce soit ne peut jamais dépendre de tout ce qui se passe dans uneIO
action.Je dirais que cela se divise en deux choses:
convert
dépend de l'état du fichier.convert
fonction fait , cela ne dépend pas de l'état du fichier.convert
est 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
convert
de 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,convert
doit le gérer correctement. Et le fait que la pureté limite ce quiconvert
peut être fait avec son entrée simplifie ce raisonnement.Donc, si
convert
produit des résultats incorrects à partir de certains arguments, et luireadFile
donne un tel argument, nous ne voyons pas cela comme un bogue introduit par l' état . C'est un bug dans une fonction pure!la source
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:
Il est vrai que la
main
fonction Haskell "vit"IO
, mais les vrais programmes Haskell sont encouragés à ne pas l'utiliserIO
chaque 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 typeIO
.Je dirais donc que dans votre dernier exemple, vous l'avez fait à l'envers:
main
est impur (car il lit et écrit des fichiers) mais les fonctions de base commeconvert
sont pures.la source
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:
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,
name
impose 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.
la source
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()
etwrite_file()
vient parfaitement naturellement parce que, bien qu'ilconvert()
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 pourread_file()
etwrite_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()
(disonsread_config_file()
). Dans Haskell lorsque vous déclarezconvert :: String -> String
ou similaire, sansIO
monade, 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 modifierconvert
pour 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 pourread_config_file
sortirconvert
et transmettre son résultatconvert
, en maintenant la pureté.)la source