Dans la programmation fonctionnelle, les variables locales modifiables sans effets secondaires sont-elles toujours considérées comme une «mauvaise pratique»?

23

Le fait d'avoir des variables locales mutables dans une fonction qui ne sont utilisées qu'en interne (par exemple, la fonction n'a pas d'effets secondaires, du moins pas intentionnellement) est-il toujours considéré comme "non fonctionnel"?

Par exemple, dans la vérification de style de cours "Programmation fonctionnelle avec Scala", toute varutilisation est considérée comme mauvaise

Ma question, si la fonction n'a pas d'effets secondaires, est-elle toujours déconseillée d'écrire du code de style impératif?

Par exemple, au lieu d'utiliser la récursivité de queue avec le modèle d'accumulateur, qu'est-ce qui ne va pas avec faire une boucle for locale et créer une mutable localeListBuffer et l'ajouter, tant que l'entrée n'est pas modifiée?

Si la réponse est "oui, ils sont toujours découragés, même s'il n'y a pas d'effets secondaires", alors quelle est la raison?

Eran Medan
la source
3
Tous les conseils, exhortations, etc. sur le sujet que j'ai jamais entendu parler de l' état mutable partagé comme source de complexité. Ce cours est-il destiné uniquement aux débutants? Il s'agit alors probablement d'une simplification délibérée bien intentionnée.
Kilian Foth du
3
@KilianFoth: L'état mutable partagé est un problème dans les contextes multithread, mais l'état mutable non partagé peut aussi rendre les programmes difficiles à raisonner.
Michael Shaw
1
Je pense que l'utilisation d'une variable locale mutable n'est pas nécessairement une mauvaise pratique, mais ce n'est pas du "style fonctionnel": je pense que le but du cours Scala (que j'ai suivi l'automne dernier) est de vous enseigner la programmation dans un style fonctionnel. Une fois que vous pouvez clairement distinguer entre le style fonctionnel et impératif, vous pouvez décider quand utiliser lequel (dans le cas où votre langage de programmation autorise les deux). varest toujours non fonctionnel. Scala a des valeurs paresseuses et une optimisation de la récursivité de la queue, ce qui permet d'éviter complètement les variables.
Giorgio

Réponses:

17

La seule chose qui est une mauvaise pratique sans équivoque ici est de prétendre que quelque chose est une fonction pure quand il ne l'est pas.

Si les variables mutables sont utilisées d'une manière qui est vraiment et complètement autonome, la fonction est purement externe et tout le monde est content. Haskell le supporte en fait explicitement , le système de types garantissant même que les références mutables ne peuvent pas être utilisées en dehors de la fonction qui les crée.

Cela dit, je pense que parler des "effets secondaires" n'est pas la meilleure façon de voir les choses (et c'est pourquoi j'ai dit "pur" ci-dessus). Tout ce qui crée une dépendance entre la fonction et l'état externe rend les choses plus difficiles à raisonner, et cela inclut des choses comme connaître l'heure actuelle ou utiliser un état mutable caché d'une manière non thread-safe.

CA McCann
la source
16

Le problème n'est pas la mutabilité en soi, c'est un manque de transparence référentielle.

Une chose référentiellement transparente et une référence à celle-ci doivent toujours être égales, donc une fonction référentiellement transparente retournera toujours les mêmes résultats pour un ensemble donné d'entrées et une "variable" référentiellement transparente est vraiment une valeur plutôt qu'une variable, car elle ne peut pas changer. Vous pouvez créer une fonction référentiellement transparente qui contient une variable mutable; ce n'est pas un problème. Il peut cependant être plus difficile de garantir que la fonction est référentiellement transparente, selon ce que vous faites.

Il y a un cas où je peux penser où la mutabilité doit être utilisée pour faire quelque chose de très fonctionnel: la mémorisation. La mémorisation consiste à mettre en cache les valeurs d'une fonction, elles ne doivent donc pas être recalculées; il est référentiellement transparent, mais il utilise une mutation.

Mais en général, la transparence référentielle et l'immuabilité vont de pair, à part une variable locale mutable dans une fonction et une mémorisation référentiellement transparente, je ne suis pas sûr qu'il existe d'autres exemples où ce n'est pas le cas.

Michael Shaw
la source
4
Votre point sur la mémorisation est très bon. Notez que Haskell met fortement l'accent sur la transparence référentielle pour la programmation, mais le comportement de mémorisation de l'évaluation paresseuse implique une quantité stupéfiante de mutation effectuée par le langage d'exécution en coulisses.
CA McCann
@CA McCann: Je pense que ce que vous dites est très important: dans un langage fonctionnel, le runtime peut utiliser la mutation pour optimiser le calcul mais il n'y a pas de construction dans le langage qui permette au programmeur d'utiliser la mutation. Un autre exemple est une boucle while avec une variable de boucle: dans Haskell, vous pouvez écrire une fonction récursive de queue qui peut être implémentée avec une variable mutable (pour éviter d'utiliser la pile), mais ce que le programmeur voit sont des arguments de fonction immuables qui sont passés d'un appeler au suivant.
Giorgio
@Michael Shaw: +1 pour "Le problème n'est pas la mutabilité en soi, c'est un manque de transparence référentielle." Vous pouvez peut-être citer le langage Clean dans lequel vous avez des types d'unicité: ils permettent la mutabilité mais garantissent toujours la transparence référentielle.
Giorgio
@Giorgio: Je ne sais vraiment rien de Clean, bien que j'en ai entendu parler de temps en temps. Je devrais peut-être y jeter un œil.
Michael Shaw
@Michael Shaw: Je ne connais pas grand-chose à Clean, mais je sais qu'il utilise des types d'unicité pour assurer la transparence référentielle. Fondamentalement, vous pouvez modifier un objet de données à condition qu'après la modification, vous n'ayez aucune référence à l'ancienne valeur. OMI, cela illustre votre point: la transparence référentielle est le point le plus important, et l'immuabilité n'est qu'un moyen possible de le garantir.
Giorgio
8

Ce n'est pas vraiment bon de résumer cela en "bonne pratique" vs "mauvaise pratique". Scala prend en charge les valeurs mutables car elles résolvent certains problèmes bien mieux que les valeurs immuables, à savoir celles qui sont de nature itérative.

Pour la perspective, je suis assez sûr que via CanBuildFrompresque toutes les structures immuables fournies par scala font une sorte de mutation en interne. Le fait est que ce qu'ils exposent est immuable. Garder autant de valeurs immuables que possible permet de rendre le programme plus facile à raisonner et moins sujet aux erreurs .

Cela ne signifie pas que vous devez nécessairement éviter les structures et valeurs mutables en interne lorsque vous rencontrez un problème mieux adapté à la mutabilité.

Dans cet esprit, de nombreux problèmes qui nécessitent généralement des variables modifiables (telles que la boucle) peuvent être mieux résolus avec de nombreuses fonctions d'ordre supérieur fournies par des langages comme Scala (carte / filtre / pli). Soyez conscient de ceux-ci.

KChaloux
la source
2
Oui, je n'ai presque jamais besoin d'une boucle for lorsque j'utilise les collections de Scala. map, filter, foldLeftEt forEach faire l'affaire la plupart du temps, mais quand ils ne le font pas, être capable de sentir que je suis « OK » pour revenir à la force brute du code impératif est agréable. (tant qu'il n'y a pas d'effets secondaires bien sûr)
Eran Medan
3

Mis à part les problèmes potentiels avec la sécurité des threads, vous perdez également généralement beaucoup de sécurité de type. Les boucles impératives ont un type de retour Unitet peuvent prendre à peu près n'importe quelle expression pour les entrées. Les fonctions d'ordre supérieur et même la récursivité ont une sémantique et des types beaucoup plus précis.

Vous avez également beaucoup plus d'options pour le traitement des conteneurs fonctionnels qu'avec les boucles impératives. Avec impératif, vous avez essentiellement for, whileet des variations mineures sur ces deux comme do...whileet foreach.

Dans fonctionnel, vous avez agréger, compter, filtrer, trouver, flatMap, fold, groupBy, lastIndexWhere, map, maxBy, minBy, partition, scan, sortBy, sortWith, span et takeWhile, pour n'en nommer que quelques-uns plus courants de Scala. bibliothèque standard. Lorsque vous vous habituez à les avoir, les forboucles impératives semblent trop basiques en comparaison.

La seule vraie raison d'utiliser la mutabilité locale est très occasionnellement pour les performances.

Karl Bielefeldt
la source
2

Je dirais que c'est généralement correct. De plus, générer des structures de cette manière peut être un bon moyen d'améliorer les performances dans certains cas. Clojure a résolu ce problème en fournissant des structures de données transitoires .

L'idée de base est de permettre des mutations locales dans une portée limitée, puis de geler la structure avant de la renvoyer. De cette façon, votre utilisateur peut toujours raisonner sur votre code comme s'il était pur, mais vous pouvez effectuer des transformations sur place lorsque vous en avez besoin.

Comme le dit le lien:

Si un arbre tombe dans les bois, fait-il du bruit? Si une fonction pure mute certaines données locales afin de produire une valeur de retour immuable, est-ce correct?

Simon Bergot
la source
2

Le fait de ne pas avoir de variables locales modifiables présente un avantage: cela rend la fonction plus conviviale pour les threads.

J'ai été brûlé par une telle variable locale (pas dans mon code, ni la source), provoquant une corruption de données à faible probabilité. La sécurité des fils n'a pas été mentionnée d'une manière ou d'une autre, aucun état n'a persisté pendant les appels et il n'y a eu aucun effet secondaire. Il ne m'est pas venu à l'esprit qu'il pourrait ne pas être sûr pour les threads, chasser une corruption de données aléatoire de 1 sur 100 000 est une douleur royale.

Loren Pechtel
la source