Pourquoi les effets secondaires sont-ils considérés comme mauvais dans la programmation fonctionnelle?

softwareengineering.stackexchange https://softwareengineering.stackexchange.com/questions/15269

Question

Je pense que les effets secondaires sont un phénomène naturel. Mais c'est quelque chose comme le tabou dans les langages fonctionnels. Quelles sont les raisons?

Ma question est spécifique au style de programmation fonctionnelle. Tous les langages de programmation / paradigmes pas.

Était-ce utile?

La solution

Écrire vos fonctions / méthodes sans effets secondaires - donc ils sont Fonctions pures - facilite la raison de l'exactitude de votre programme.

Il facilite également la composition de ces fonctions pour créer de nouveaux comportements.

Il rend également certaines optimisations possibles, où le compilateur peut par exemple Mémoire les résultats des fonctions ou utiliser l'élimination commune de la sous-expression.

Edit: à la demande de Benjol: car une grande partie de votre état est stocké dans la pile (flux de données, pas le flux de contrôle, comme Jonas l'a appelé ici), vous pouvez paralléliser ou réorganiser autrement l'exécution des parties de votre calcul qui sont indépendantes les unes des autres. Vous pouvez facilement trouver ces pièces indépendantes car une pièce ne fournit pas d'entrées à l'autre.

Dans les environnements avec des débogueurs qui vous permettent de faire reculer la pile et de reprendre l'informatique (comme SmallTalk), avoir des fonctions pures signifie que vous pouvez très facilement voir comment une valeur change, car les états précédents sont disponibles pour l'inspection. Dans un calcul vif de mutation, à moins que vous ajoutiez explicitement les actions DO / annulent votre structure ou votre algorithme, vous ne pouvez pas voir l'historique du calcul. (Cela revient au premier paragraphe: l'écriture de fonctions pures facilite la inspecter l'exactitude de votre programme.)

Autres conseils

À partir d'un article sur Programmation fonctionnelle:

En pratique, les applications doivent avoir des effets secondaires. Simon Peyton-Jones, un contributeur majeur au langage de programmation fonctionnelle Haskell, a déclaré ce qui suit: "En fin de compte, tout programme doit manipuler l'état. Un programme qui n'a aucun effet secondaire est une sorte de boîte noire. Tout ce que vous pouvez dire est que la boîte devient plus chaude. " (http://oscon.blip.tv/file/324976) La clé est de limiter les effets secondaires, de les identifier clairement et d'éviter de les diffuser tout au long du code.

Vous avez un mauvais problème, la programmation fonctionnelle favorise la limitation des effets secondaires pour rendre les programmes faciles à comprendre et à optimiser. Même Haskell vous permet d'écrire dans les fichiers.

Essentiellement, ce que je dis, c'est que les programmeurs fonctionnels ne pensent pas que les effets secondaires sont mauvais, ils pensent simplement que limiter l'utilisation des effets secondaires est bon. Je sais que cela peut sembler une distinction si simple, mais cela fait toute la différence.

Quelques notes:

  • Les fonctions sans effets secondaires peuvent être exécutées en parallèle, tandis que les fonctions avec des effets secondaires nécessitent généralement une sorte de synchronisation.

  • Les fonctions sans effets secondaires permettent une optimisation plus agressive (par exemple en utilisant transparente un cache de résultat), car tant que nous obtenons le bon résultat, peu importe si la fonction était ou non vraiment réalisé

Je travaille principalement dans le code fonctionnel maintenant, et dans cette perspective, il semble aveuglément évident. Les effets secondaires créent un énorme Charge mentale pour les programmeurs essayant de lire et de comprendre le code. Vous ne remarquez pas ce fardeau tant que vous n'en êtes pas libre pendant un certain temps, puis vous devez soudainement lire du code avec des effets secondaires à nouveau.

Considérez ce simple exemple:

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

Dans un langage fonctionnel, je connaître ce foo est encore 42. Je n'ai même pas à à voir Au code entre les deux, beaucoup moins le comprendre ou regarder les implémentations des fonctions qu'il appelle.

Tout cela sur la concurrence et la parallélisation et l'optimisation est agréable, mais c'est ce que les informaticiens ont mis sur la brochure. Ne pas avoir à se demander qui mute votre variable et quand est ce que j'aime vraiment dans la pratique quotidienne.

Peu ou pas de langues rendent impossible de provoquer des effets secondaires. Les langues qui étaient complètement libres d'effets secondaires seraient prohibitivement difficiles (presque impossibles) à utiliser, sauf à une capacité très limitée.

Pourquoi les effets secondaires sont-ils considérés comme mauvais?

Parce qu'ils rendent beaucoup plus difficile de raisonner exactement à ce qu'un programme fait et à prouver qu'il fait ce que vous attendez.

À un niveau très élevé, imaginez tester un site Web entier à 3 niveaux avec seulement des tests de boîte noire. Bien sûr, c'est faisable, selon l'échelle. Mais il y a certainement beaucoup de duplication en cours. Et si là est Un bogue (qui est lié à un effet secondaire), alors vous pourriez potentiellement casser l'ensemble du système pour des tests supplémentaires, jusqu'à ce que le bogue soit diagnostiqué et corrigé, et que le correctif soit déployé dans l'environnement de test.

Avantages

Maintenant, réduisez cela. Si vous étiez assez bon pour écrire du code gratuit à effet secondaire, à quel point seriez-vous plus rapide pour raisonner sur ce que certains code existants ont fait? Combien pouvez-vous écrire des tests unitaires plus rapides? Dans quelle mesure vous sentez-vous confiant que le code sans effets secondaires était garanti sans bug et que les utilisateurs pouvaient limiter leur exposition à tout bogue a fait ont?

Si le code n'a pas d'effets secondaires, le compilateur peut également avoir des optimisations supplémentaires qu'elle pourrait effectuer. Il peut être beaucoup plus facile d'implémenter ces optimisations. Il peut être beaucoup plus facile de conceptualiser une optimisation pour le code sans effet secondaire, ce qui signifie que votre fournisseur de compilateur pourrait implémenter des optimisations difficiles à impossibles dans le code avec des effets secondaires.

La concurrence est également considérablement plus simple à implémenter, à générer automatiquement et à optimiser quand le code n'a pas d'effets secondaires. En effet, toutes les pièces peuvent être évaluées en toute sécurité dans n'importe quel ordre. Permettre aux programmeurs d'écrire du code très concurrent est largement considéré comme le prochain grand défi que l'informatique doit relever, et l'une des rares haies restantes contre la loi de Moore.

Les effets secondaires sont comme des «fuites» dans votre code qui devra être gérée plus tard, soit par vous, soit par un collègue sans méfiance.

Les langues fonctionnelles évitent les variables d'état et les données mutables comme moyen de rendre le code moins dépendant du contexte et plus modulaire. La modularité assure que le travail d'un développeur n'affectera pas / ne sapera pas le travail d'une autre.

La mise à l'échelle du développement avec la taille de l'équipe est un "Saint Graal" du développement de logiciels aujourd'hui. Lorsque vous travaillez avec d'autres programmeurs, peu de choses sont aussi importantes que la modularité. Même les effets secondaires logiques les plus simples rendent la collaboration extrêmement difficile.

Eh bien, à mon humble avis, c'est assez hypocrite. Personne n'aime les effets secondaires, mais tout le monde en a besoin.

Ce qui est si dangereux dans les effets secondaires, c'est que si vous appelez une fonction, cela a peut-être un effet non seulement sur la façon dont la fonction se comporte lorsqu'elle est appelée la prochaine fois, mais peut-être qu'il a cet effet sur d'autres fonctions. Ainsi, les effets secondaires introduisent un comportement imprévisible et des dépendances non triviales.

La programmation de paradigmes tels que OO et fonctionnels aborde ce problème. OO réduit le problème en imposant une séparation des préoccupations. Cela signifie que l'état d'application, qui se compose de nombreuses données mutables, est encapsulé en objets, dont chacun est responsable du maintien de son propre état uniquement. De cette façon, le risque de dépendances est réduit et les problèmes sont beaucoup plus isolés et plus faciles à suivre.

La programmation fonctionnelle adopte une approche beaucoup plus radicale, où l'état d'application est tout simplement immuable du point de vue du programmeur. C'est une bonne idée, mais rend la langue inutile à elle seule. Pourquoi? Parce que toute opération d'E / S a des effets secondaires. Dès que vous lisez un flux d'entrée, votre état d'application est susceptible de changer, car la prochaine fois que vous invoquerez la même fonction, le résultat est susceptible d'être différent. Vous pouvez lire différentes données, ou - également une possibilité - l'opération peut échouer. Il en va de même pour la sortie. Même la sortie est une opération avec des effets secondaires. Ce n'est rien que vous réalisez souvent de nos jours, mais imaginez que vous n'avez que 20k pour votre sortie et si vous sortez plus, votre application se bloque parce que vous êtes hors de l'espace disque ou autre.

Alors oui, les effets secondaires sont désagréables et dangereux du point de vue d'un programmeur. La plupart des bogues proviennent de la façon dont certaines parties de l'état d'application sont verrouillées de manière presque obscure, à travers des effets secondaires non consistés et souvent inutiles. Du point de vue d'un utilisateur, les effets secondaires sont le point d'utilisation d'un ordinateur. Ils ne se soucient pas de ce qui se passe à l'intérieur ou de la façon dont il est organisé. Ils font quelque chose et s'attendent à ce que l'ordinateur change en conséquence.

Tout effet secondaire introduit des paramètres d'entrée / sortie supplémentaires qui doivent être pris en compte lors des tests.

Cela rend la validation du code beaucoup plus complexe car l'environnement ne peut pas se limiter à la validation du code, mais doit apporter une partie ou la totalité de l'environnement environnant (le global qui est mis à jour dans ce code là-bas, qui à son tour dépend de cela Code, qui à son tour dépend de la vie à l'intérieur d'un serveur Java EE complet ....)

En essayant d'éviter les effets secondaires, vous limitez la quantité d'externalisme nécessaire pour exécuter le code.

D'après mon expérience, une bonne conception dans la programmation orientée objet oblige l'utilisation de fonctions qui ont des effets secondaires.

Par exemple, prenez une application de bureau d'interface utilisateur de base. J'ai peut-être un programme de course qui a sur son graphique d'objet tas représentant l'état actuel du modèle de domaine de mon programme. Les messages arrivent aux objets de ce graphique (par exemple, via des appels de méthodes invoqués à partir du contrôleur de couche d'interface utilisateur). Le graphique d'objet (modèle de domaine) sur le tas est modifié en réponse aux messages. Les observateurs du modèle sont informés de tout changement, l'interface utilisateur et peut-être que d'autres ressources sont modifiées.

Loin d'être mauvais, la disposition correcte de ces effets secondaires modifiant et modifiant l'écran est au cœur de la conception OO (dans ce cas, le modèle MVC).

Bien sûr, cela ne signifie pas que vos méthodes devraient avoir des effets secondaires arbitaires. Et les fonctions libres de l'effet secondaire ont une place pour améliorer la lecture et parfois les performances de votre code.

Le mal est un peu exagéré. Tout dépend du contexte de l'utilisation de la langue.

Une autre considération pour ceux déjà mentionnés est qu'il rend les preuves d'exactitude d'un programme beaucoup plus simples s'il n'y a pas d'effets secondaires fonctionnels.

Comme l'ont souligné les questions ci-dessus, les langues fonctionnelles ne sont pas aussi empêcher Le code des effets secondaires a-t-il comme des outils pour gérer quels effets secondaires peuvent se produire dans un morceau de code donné et quand.

Cela s'avère avoir des conséquences très intéressantes. Tout d'abord, et plus évidemment, il y a de nombreuses choses que vous pouvez faire avec du code sans effet secondaire, qui a déjà été décrit. Mais il y a aussi d'autres choses que nous pouvons faire, même lorsque vous travaillez avec du code qui a des effets secondaires:

  • Dans le code avec un état mutable, nous pouvons gérer la portée de l'état de manière à nous assurer statiquement qu'elle ne peut pas fuir en dehors d'une fonction donnée, ce qui nous permet de collecter les ordures sans compter de référence ou schémas de style mark-and-sweep , mais assurez-vous toujours qu'aucune référence ne survit. Les mêmes garanties sont également utiles pour maintenir des informations sensibles à la confidentialité, etc. (cela peut être réalisé en utilisant la Saint-Monad à Haskell)
  • Lors de la modification de l'état partagé dans plusieurs threads, nous pouvons éviter le besoin de verrous en suivant les modifications et en effectuant une mise à jour atomique à la fin d'une transaction, ou en faisant reculer la transaction et en la répétant si un autre thread a effectué une modification contradictoire. Ceci n'est réalisable que car nous pouvons nous assurer que le code n'a aucun effet autre que les modifications de l'État (que nous pouvons abandonner avec plaisir). Ceci est effectué par la STM (Software Transactional Memory) Monad dans Haskell.
  • Nous pouvons suivre les effets du code et le sable trivialement le sable, filtrant tous les effets dont il peut avoir besoin pour être sûr qu'il est sûr, permettant ainsi (par exemple) Le code entré utilisateur à exécuter en toute sécurité sur un site Web

Dans les bases de code complexes, les interactions complexes des effets secondaires sont la chose la plus difficile à laquelle je trouve. Je ne peux parler que personnellement étant donné le fonctionnement de mon cerveau. Les effets secondaires et les états persistants et les intrants mutés et ainsi de suite me font devoir penser à «quand» et «où» les choses sont des raisons de l'exactitude, pas seulement «ce qui se passe» dans chaque fonction individuelle.

Je ne peux pas me concentrer sur "quoi". Je ne peux pas conclure après avoir testé soigneusement une fonction qui provoque des effets secondaires qu'il répartira un air de fiabilité tout au long du code en l'utilisant, car les appelants pourraient encore l'utiliser en l'appelant au mauvais moment, du mauvais fil, dans le mauvais ordre. Pendant ce temps, une fonction qui ne provoque aucun effet secondaire et renvoie simplement une nouvelle sortie compte tenu d'une entrée (sans toucher l'entrée) est à peu près impossible à abuser de cette manière.

Mais je suis un type pragmatique, je pense, ou du moins essayer de l'être, et je ne pense pas que nous devons nécessairement éliminer tous les effets secondaires au minimum le plus à la raison de l'exactitude de notre code (à tout le moins Je trouverais cela très difficile à faire dans des langues comme c). Là où je trouve très difficile de raisonner de l'exactitude, c'est lorsque nous avons la combinaison de flux de contrôle complexes et d'effets secondaires.

Les flux de contrôle complexes pour moi sont ceux qui sont de nature graphique, souvent récursifs ou récursifs (files d'attente d'événements, par exemple, qui n'appellent pas directement des événements mais sont "de type récursif"), peut-être faire des choses Dans le processus de traverser une structure de graphe liée réelle ou de traiter une file d'attente d'événements non homogène qui contient un mélange éclectique d'événements pour nous procéder à toutes sortes de différentes parties de la base de code et tout déclenchant différents effets secondaires. Si vous essayiez de tirer tous les endroits que vous finirez par vous retrouver dans le code, il ressemblerait à un graphique complexe et potentiellement avec les nœuds dans le graphique que vous ne vous attendiez pas provoquant des effets secondaires, cela signifie que vous pourriez non seulement être surpris par les fonctions appelées, mais aussi quels effets secondaires se produisent pendant cette période et l'ordre dans lequel ils se produisent.

Les langages fonctionnels peuvent avoir des flux de contrôle extrêmement complexes et récursifs, mais le résultat est si facile à comprendre en termes d'exactitude car il n'y a pas toutes sortes d'effets secondaires éclectiques en cours dans le processus. Ce n'est que lorsque des flux de contrôle complexes rencontrent des effets secondaires éclectiques que je trouve que les maux de tête à la tête pour essayer de comprendre l'intégralité de ce qui se passe et si cela fera toujours la bonne chose.

Donc, quand j'ai ces cas, je trouve souvent très difficile, voire impossible, de me sentir très confiant quant à l'exactitude d'un tel code, et encore moins très confiant que je peux apporter des modifications à un tel code sans trébucher sur quelque chose d'inattendu. Ainsi, la solution pour moi est soit simplifier le flux de contrôle, soit minimiser / unifier les effets secondaires (en unifiant, je veux dire comme provoquant un seul type d'effet secondaire à beaucoup de choses pendant une phase particulière du système, pas deux ou trois ou un douzaine). J'ai besoin d'une de ces deux choses pour permettre à mon cerveau de simplet de se sentir confiant quant à l'exactitude du code qui existe et à l'exactitude des changements que je présente. Il est assez facile d'être confiant quant à l'exactitude du code introduisant des effets secondaires si les effets secondaires sont uniformes et simples avec le flux de contrôle, comme ainsi:

for each pixel in an image:
    make it red

Il est assez facile de raisonner de l'exactitude d'un tel code, mais principalement parce que les effets secondaires sont si uniformes et le flux de contrôle est si simple. Mais disons que nous avions du code comme ceci:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove edge
         remove vertex

Ensuite, c'est un pseudocode ridiculement simplifié qui impliquerait généralement beaucoup plus de fonctions et de boucles imbriquées et beaucoup plus de choses qui devraient continuer (mettre à jour plusieurs cartes de texture, poids osseux, états de sélection, etc.), mais même le pseudocode rend si difficile à Raison sur l'exactitude en raison de l'interaction du flux de contrôle complexe de type graphique et des effets secondaires en cours. Ainsi, une stratégie pour simplifier est de reporter le traitement et de se concentrer uniquement sur un type d'effet secondaire à la fois:

for each vertex to remove:
     mark connected edges
for each marked edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked edge:
     remove edge
for each vertex to remove:
     remove vertex

... quelque chose à cet effet comme une itération de simplification. Cela signifie que nous passons à travers les données plusieurs fois, ce qui entraîne définitivement un coût de calcul, mais nous constatons souvent que nous pouvons multithread un tel code résultant plus facilement, maintenant que les effets secondaires et les flux de contrôle ont pris cette nature uniforme et plus simple. De plus Utilisation de Bitmasks et FFS). Mais surtout, je trouve la deuxième version beaucoup plus facile à raisonner en termes d'exactitude ainsi que de changement sans provoquer des bogues. C'est ainsi que je m'approche quand même et j'applique le même type d'esprit pour simplifier le traitement de maillage ci-dessus que la simplification de la manipulation des événements et ainsi de suite - des boucles plus homogènes avec des flux de contrôle simples morts provoquant des effets secondaires uniformes.

Et après tout, nous avons besoin d'effets secondaires pour se produire à un moment donné, sinon nous aurions simplement des fonctions qui ne publient nulle part où aller. Souvent, nous devons enregistrer quelque chose dans un fichier, afficher quelque chose sur un écran, envoyer les données via une prise, quelque chose de ce type, et toutes ces choses sont des effets secondaires. Mais nous pouvons certainement réduire le nombre d'effets secondaires superflus qui continuent, et réduire également le nombre d'effets secondaires en cours lorsque les flux de contrôle sont très compliqués, et je pense qu'il serait beaucoup plus facile d'éviter les insectes si nous l'avons fait.

Ce n'est pas mal. Mon avis, il est nécessaire de distinguer les deux types de fonction - avec des effets secondaires et sans. La fonction sans effets secondaires: - Renvoie toujours le même avec les mêmes arguments, donc par exemple une telle fonction sans aucun argument n'a aucun sens. - Cela signifie également que l'ordre dans ce que sont appelées de telles fonctions ne jouent aucun rôle - doit être en mesure d'exécuter et peut être débogué uniquement (!), Sans un autre code. Et maintenant, lol, regardez ce que fait Junit. Une fonction avec des effets secondaires: - a une sorte de "fuites", ce qui peut être mis en évidence automatiquement - il est très important en débogage et en recherchant des erreurs, ce qui est généralement causé par des effets secondaires. - Toute fonction avec des effets secondaires a également une "partie" d'elle-même sans effets secondaires, ce qui peut également être séparé automatiquement. Donc, le mal sont ces effets secondaires, ce qui produit des erreurs difficiles à suivre.

Licencié sous: CC-BY-SA avec attribution
scroll top