web-dev-qa-db-fra.com

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

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

Ma question est spécifique au style de programmation fonctionnel. Pas tous les langages/paradigmes de programmation.

70
Gulshan

Écrire vos fonctions/méthodes sans effets secondaires - donc ce sont des fonctions pures - permet de raisonner plus facilement sur l'exactitude de votre programme.

Il facilite également la composition de ces fonctions pour créer un nouveau comportement.

Il permet également certaines optimisations, où le compilateur peut par exemple mémoriser les résultats des fonctions, ou utiliser Common Subexpression Elimination.

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

Dans les environnements avec des débogueurs qui vous permettent de restaurer la pile et de reprendre le calcul (comme Smalltalk), le fait d'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 inspection. Dans un calcul lourd de mutations, à moins que vous n'ajoutiez explicitement des actions do/undo à votre structure ou algorithme, vous ne pouvez pas voir l'historique du calcul. (Cela revient au premier paragraphe: l'écriture de fonctions pures facilite l'inspection de l'exactitude de votre programme.)

73
Frank Shearar

Vous vous trompez, 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 des 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 aussi simple, mais cela fait toute la différence.

24
ChaosPandion

Extrait d'un article sur Programmation fonctionnelle :

En pratique, les applications doivent avoir certains effets secondaires. Simon Peyton-Jones, un contributeur majeur au langage de programmation fonctionnel 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 chauffe. " ( http://oscon.blip.tv/file/324976 ) La clé est de limiter les effets secondaires, de les identifier clairement et d'éviter de les disperser dans le code.

23
Peter Stuifzand

Quelques notes:

  • Les fonctions sans effets secondaires peuvent être exécutées en toute simplicité en parallèle, tandis que les fonctions avec 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 de manière transparente un cache de résultats), car tant que nous obtenons le bon résultat, peu importe que la fonction soit ou non vraiment réalisé

13
user281377

Je travaille principalement dans le code fonctionnel maintenant, et de ce point de vue, cela semble aveuglément évident. Les effets secondaires créent une charge mentale énorme pour les programmeurs qui essaient de lire et de comprendre le code. Vous ne remarquez pas ce fardeau jusqu'à ce que vous en soyez libéré pendant un certain temps, puis devez soudainement relire le code avec les effets secondaires.

Considérez cet exemple simple:

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 sais que foo a encore 42 ans. Je n'ai même pas besoin de regardez le code entre les deux, encore moins le comprenez, ou regardez les implémentations des fonctions qu'il appelle.

Tout ce qui concerne la concurrence, la parallélisation et l'optimisation est bien, mais c'est ce que les informaticiens mettent sur la brochure. Ne pas avoir à me demander qui mute votre variable et quand est ce que j'aime vraiment dans la pratique quotidienne.

11
Karl Bielefeldt

Peu ou pas de langues ne permettent pas de provoquer des effets secondaires. Les langues totalement exemptes d'effets secondaires seraient d'une utilisation prohibitive (presque impossible), sauf dans 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 de prouver qu'il fait ce que vous attendez de lui.

À 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 dédoublements. Et s'il y a est un bogue (qui est lié à un effet secondaire), alors vous pourriez potentiellement casser tout le système pour des tests supplémentaires, jusqu'à ce que le bogue soit diagnostiqué et corrigé, et que le correctif soit déployée dans l'environnement de test.

Avantages

Maintenant, réduisez cela. Si vous étiez assez bon pour écrire du code libre à effets secondaires, à quelle vitesse seriez-vous plus rapide à raisonner sur ce que faisait le code existant? Combien plus rapide pourriez-vous écrire des tests unitaires? Dans quelle mesure pensez-vous que le code sans effets secondaires est garanti sans bogue et que les utilisateurs peuvent limiter leur exposition aux bogues qu'il a fait?

Si le code n'a pas d'effets secondaires, le compilateur peut également avoir des optimisations supplémentaires qu'il pourrait effectuer. Il peut être beaucoup plus facile d'implémenter ces optimisations. Il peut être beaucoup plus facile de même conceptualiser une optimisation pour le code libre d'effets secondaires, ce qui signifie que votre fournisseur de compilateur peut 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 lorsque 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 hautement simultané est largement considéré comme le prochain grand défi que l'informatique doit relever, et l'une des rares couvertures restantes contre loi de Moore .

6

Les effets secondaires sont comme des "fuites" dans votre code qui devront être traitées plus tard, soit par vous, soit par un collègue sans méfiance.

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

La mise à l'échelle du taux de développement avec la taille de l'équipe est aujourd'hui un "Saint Graal" du développement logiciel. 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.

4
Ami

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 à propos des effets secondaires, c'est que si vous appelez une fonction, cela a peut-être un effet non seulement sur le comportement de la fonction lors de son prochain appel, mais peut aussi avoir cet effet sur d'autres fonctions. Ainsi, les effets secondaires introduisent un comportement imprévisible et des dépendances non triviales.

Des paradigmes de programmation tels que OO et fonctionnel répondent tous deux à ce problème. OO réduit le problème en imposant une séparation des préoccupations. Cela signifie que l'état de l'application, qui consiste en de nombreuses données mutables sont encapsulées dans des objets dont chacun est responsable du maintien de son propre état, ce qui réduit le risque de dépendances 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 de l'application est tout simplement immuable du point de vue du programmeur. C'est une bonne idée, mais rend la langue inutile en soi. Pourquoi? Parce que TOUTE opération d'E/S a des effets secondaires. Dès que vous lisez à partir d'un flux d'entrée, l'état de votre application est susceptible de changer, car la prochaine fois que vous appellerez la même fonction, le résultat sera probablement différent. Il se peut que vous lisiez différentes données ou - également une possibilité - l'opération puisse é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 20 Ko pour votre sortie et si vous en produisez plus, votre application se bloque parce que vous manquez d'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 de l'application sont verrouillées de manière presque obscure, par le biais d'effets secondaires non considérés et souvent inutiles. Du point de vue d'un utilisateur, les effets secondaires sont le point d'utiliser un ordinateur. Ils ne se soucient pas de ce qui se passe à l'intérieur ni de la façon dont cela est organisé. Ils font quelque chose et s'attendent à ce que l'ordinateur CHANGE en conséquence.

4
back2dos

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 être limité au seul code en cours de validation, mais doit faire entrer tout ou partie de l'environnement environnant (le global qui est mis à jour vit dans ce code là-bas, qui à son tour dépend de cela code, qui à son tour dépend de vivre à 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.

2
user1249

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

Par exemple, prenez une application de bureau d'interface utilisateur de base. Je peux avoir un programme en cours d'exécution qui a sur son tas un graphique d'objet 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 depuis le contrôleur de couche UI). 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 d'autres ressources sont modifiées.

Loin d'être mauvais, l'arrangement correct de ces effets secondaires de modification de tas et de modification d'é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 arbitraires. Et les fonctions sans effets secondaires ont une place dans l'amélioration de la lisibilité et parfois des performances de votre code.

1
flamingpenguin

Comme les questions ci-dessus l'ont souligné, les langages fonctionnels n'empêchent pas tellement le code d'avoir des effets secondaires, car ils nous fournissent des outils pour gérer les effets secondaires arriver dans un morceau de code donné et quand.

Cela s'avère avoir des conséquences très intéressantes. Tout d'abord, et de toute évidence, il existe de nombreuses choses que vous pouvez faire avec du code libre à effets secondaires, qui ont déjà été décrites. Mais il y a d'autres choses que nous pouvons faire aussi, même lorsque nous travaillons 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 à garantir statiquement qu'il ne peut pas fuir en dehors d'une fonction donnée, ce qui nous permet de collecter les ordures sans comptage de référence ou schémas de style de marquage et de balayage , tout en étant sûr qu'aucune référence ne survit. Les mêmes garanties sont également utiles pour maintenir des informations sensibles à la vie privée, etc. (Ceci peut être réalisé en utilisant la monade ST dans haskell)
  • Lors de la modification de l'état partagé dans plusieurs threads, nous pouvons éviter d'avoir besoin de verrous en suivant les modifications et en effectuant une mise à jour atomique à la fin d'une transaction, ou en annulant la transaction et en la répétant si un autre thread a effectué une modification conflictuelle. Ceci n'est réalisable que parce que nous pouvons nous assurer que le code n'a aucun effet autre que les modifications d'état (que nous pouvons volontiers abandonner). Ceci est effectué par la monade STM (Software Transactional Memory) à Haskell.
  • nous pouvons suivre les effets du code et le sabler trivialement, filtrer tous les effets dont il peut avoir besoin pour être sûr qu'il est sûr, permettant ainsi (par exemple) le code saisi par l'utilisateur d'être exécuté en toute sécurité sur un site Web
0
Jules

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

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

0
Ilan

Dans les bases de code complexes, les interactions complexes d'effets secondaires sont la chose la plus difficile à trouver. Je ne peux parler que personnellement étant donné le fonctionnement de mon cerveau. Les effets secondaires et les états persistants et les entrées en mutation et ainsi de suite me font penser au "quand" et "où" les choses arrivent à raisonner sur la correction, pas seulement "ce" qui se passe dans chaque fonction individuelle.

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

Mais je suis un type pragmatique, je pense, ou du moins essaie de l'être, et je ne pense pas que nous devons nécessairement éliminer tous les effets secondaires au minimum pour raisonner sur l'exactitude de notre code (à tout le moins Je trouverais cela très difficile à faire dans des langages comme C). Là où je trouve qu'il est très difficile de raisonner sur l'exactitude, c'est quand 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 de manière récursive mais sont de nature "récursive"), faisant peut-être des choses dans le processus de traverser une structure de graphique 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 à traiter, nous menant à toutes sortes de parties différentes de la base de code et déclenchant tous des effets secondaires différents. Si vous avez essayé de dessiner tous les endroits que vous finirez par finir dans le code, cela ressemblerait à un graphique complexe et potentiellement avec des nœuds dans le graphique que vous n'auriez jamais pensé avoir été là à ce moment donné, et étant donné qu'ils sont tous provoquant des effets secondaires, cela signifie que vous pourriez non seulement être surpris de savoir quelles fonctions sont 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 qui se produisent dans le processus. Ce n'est que lorsque les flux de contrôle complexes rencontrent des effets secondaires éclectiques que je trouve mal à la tête d'essayer de comprendre l'intégralité de ce qui se passe et si cela fera toujours la bonne chose.

Donc, lorsque 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 à ce code sans trébucher sur quelque chose d'inattendu. Donc, la solution pour moi est de simplifier le flux de contrôle ou de minimiser/unifier les effets secondaires (en unifiant, je veux dire comme ne causer qu'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 que l'une de ces deux choses se produise pour permettre à mon cerveau simplet de se sentir confiant quant à l'exactitude du code qui existe et à l'exactitude des changements que j'introduis. Il est assez facile d'avoir confiance en l'exactitude du code introduisant des effets secondaires si les effets secondaires sont uniformes et simples avec le flux de contrôle, comme ceci:

for each pixel in an image:
    make it red

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

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 bien plus de choses qui devraient continuer (mise à jour de plusieurs cartes de texture, poids des os, états de sélection, etc.), mais même le pseudocode le rend si difficile à raison de l'exactitude en raison de l'interaction du flux de contrôle de type graphique complexe et des effets secondaires en cours. Donc, une stratégie pour simplifier cela est de différer 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 parcourons les données plusieurs fois, ce qui entraîne certainement un coût de calcul, mais nous constatons souvent que nous pouvons multithreader plus facilement ce code résultant, maintenant que les effets secondaires et les flux de contrôle ont pris cette nature uniforme et plus simple. De plus, chaque boucle peut être rendue plus conviviale pour le cache que la traversée du graphe connecté et provoquer des effets secondaires au fur et à mesure (ex: utiliser un ensemble de bits parallèles pour marquer ce qui doit être parcouru afin que nous puissions ensuite effectuer les passes différées dans un ordre séquentiel trié utilisant des masques de bit et FFS). Mais plus important encore, je trouve la deuxième version tellement plus facile à raisonner en termes d'exactitude et de changement sans provoquer de bugs. C'est donc ainsi que je l'aborde de toute façon et j'applique le même type de mentalité pour simplifier le traitement du maillage ci-dessus que pour simplifier la gestion des événements, etc. - 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 que des effets secondaires se produisent à un moment donné, ou bien nous aurions simplement des fonctions qui produisent des données sans nulle part où aller. Souvent, nous devons enregistrer quelque chose dans un fichier, afficher quelque chose sur un écran, envoyer les données via un socket, quelque chose de ce genre, et toutes ces choses sont des effets secondaires. Mais nous pouvons certainement réduire le nombre d'effets secondaires superflus qui se produisent, et également réduire le nombre d'effets secondaires qui se produisent lorsque les flux de contrôle sont très compliqués, et je pense qu'il serait beaucoup plus facile d'éviter les bogues si nous le faisions.

0
user204677