Fonctionnel pur vs dire, ne demandez pas?

14

"Le nombre idéal d'arguments pour une fonction est zéro" est tout simplement faux. Le nombre idéal d'arguments est exactement le nombre nécessaire pour permettre à votre fonction d'être sans effet secondaire. Moins que cela et vous faites inutilement impur vos fonctions, vous forçant ainsi à vous éloigner du gouffre du succès et à gravir le gradient de la douleur. Parfois, "Oncle Bob" est sur place avec ses conseils. Parfois, il se trompe de façon spectaculaire. Son avis zéro argument est un exemple de ce dernier

( Source: commentaire de @David Arno sous une autre question sur ce site )

Le commentaire a gagné un nombre spectaculaire de 133 votes positifs, c'est pourquoi j'aimerais porter une attention particulière à son mérite.

Pour autant que je sache, il y a deux façons distinctes dans la programmation: la programmation fonctionnelle pure (ce que ce commentaire est encourageant) et dites, ne demandez pas (qui de temps en temps est également recommandé sur ce site). AFAIK ces deux principes sont fondamentalement incompatibles, proches d'être opposés l'un à l'autre: le pur fonctionnel peut être résumé comme "seulement des valeurs de retour, sans effets secondaires" tandis que dire, ne pas demander peut être résumé comme "ne retourne rien, avoir que des effets secondaires ". De plus, je suis un peu perplexe parce que je pensais que dire, ne pas demander était considéré comme le cœur du paradigme OO tandis que les fonctions pures étaient considérées comme le cœur du paradigme fonctionnel - maintenant je vois des fonctions pures recommandées dans OO!

Je suppose que les développeurs devraient probablement choisir l'un de ces paradigmes et s'y tenir? Eh bien, je dois admettre que je ne pourrais jamais me résoudre à suivre non plus. Souvent, il me semble pratique de renvoyer une valeur et je ne vois pas vraiment comment puis-je atteindre ce que je veux réaliser uniquement avec des effets secondaires. Souvent, cela me semble pratique d'avoir des effets secondaires et je ne vois pas vraiment comment puis-je atteindre ce que je veux réaliser uniquement en renvoyant des valeurs. De plus, souvent (je suppose que c'est horrible), j'ai des méthodes qui font les deux.

Cependant, à partir de ces 133 votes positifs, je raisonne que la programmation fonctionnelle actuellement pure "gagne" car cela devient un consensus qu'il est supérieur de dire, ne demandez pas. Est-ce correct?

Par conséquent, sur l'exemple de ce jeu anti-modèles, j'essaie de faire : Si je voulais le mettre en conformité avec le paradigme fonctionnel pur - COMMENT?!

Il me semble raisonnable d'avoir un état de bataille. Comme il s'agit d'un jeu au tour par tour, je garde les états de bataille dans un dictionnaire (multijoueur - il peut y avoir plusieurs batailles jouées par plusieurs joueurs en même temps). Chaque fois qu'un joueur fait son tour, j'appelle une méthode appropriée sur l'état de bataille qui (a) modifie l'état en conséquence et (b) renvoie des mises à jour aux joueurs, qui sont sérialisées en JSON et leur dis simplement ce qui vient de se passer sur le planche. Je suppose que cela constitue une violation flagrante des DEUX principes et en même temps.

OK - Je pourrais faire en sorte qu'une méthode RETOURNE un état de bataille au lieu de la modifier si je le voulais vraiment. Mais! Dois-je alors tout copier inutilement dans l'état de combat juste pour retourner un état entièrement nouveau au lieu de le modifier en place?

Maintenant, peut-être que si le mouvement est une attaque, je pourrais simplement renvoyer un personnage mis à jour HP? Le problème est que ce n'est pas si simple: les règles du jeu, un coup peut et aura souvent beaucoup plus d'effets que de simplement supprimer une partie des HP d'un joueur. Par exemple, cela peut augmenter la distance entre les personnages, appliquer des effets spéciaux, etc., etc.

Il me semble tellement plus simple de simplement modifier l'état en place et de renvoyer les mises à jour ...

Mais comment un ingénieur expérimenté pourrait-il résoudre ce problème?

gaazkam
la source
9
Suivre n'importe quel paradigme est un moyen sûr d'échouer. La politique ne doit jamais l'emporter sur l'intelligence. La solution à un problème doit dépendre du problème et non de vos croyances religieuses concernant la résolution de problèmes.
John Douma
1
Je n'ai jamais posé de question ici sur quelque chose que j'ai dit auparavant. Je suis honoré. :)
David Arno

Réponses:

14

Comme la plupart des aphorismes de programmation, «dites, ne demandez pas» sacrifie la clarté pour gagner en brièveté. Il n'est pas du tout destiné à déconseiller de demander les résultats d'un calcul, il déconseille de demander les entrées d'un calcul. "Ne pas obtenir, puis calculer, puis définir, mais il est normal de renvoyer une valeur à partir d'un calcul", n'est pas aussi concis.

Auparavant, il était assez courant pour les gens d'appeler un getter, de faire un calcul à ce sujet, puis d'appeler un setter avec le résultat. C'est un signe clair que votre calcul appartient en fait à la classe sur laquelle vous avez appelé le getter. "Dites, ne demandez pas" a été inventé pour rappeler aux gens d'être à l'affût de cet anti-modèle, et cela a si bien fonctionné que maintenant certaines personnes pensent que cette partie est évidente et recherchent d'autres types de "demandes" pour éliminer. Cependant, l'aphorisme n'est appliqué utilement qu'à cette seule situation.

Les programmes fonctionnels purs n'ont jamais souffert de cet anti-modèle exact, pour la simple raison qu'il n'y a pas de setters dans ce style. Cependant, le problème plus général (et plus difficile à voir) de ne pas mélanger différents niveaux d'abstraction sémantique dans la même fonction s'applique à chaque paradigme.

Karl Bielefeldt
la source
Merci d'avoir bien expliqué "Dites, ne demandez pas".
user949300
13

L'oncle Bob et David Arno (l'auteur de la citation que vous aviez) ont tous deux d'importantes leçons que nous pouvons tirer de ce qu'ils ont écrit. Je pense que cela vaut la peine d'apprendre la leçon et d'extrapoler ensuite ce que cela signifie vraiment pour vous et votre projet.

Premièrement: la leçon de l'oncle Bob

L'oncle Bob fait valoir que plus vous avez d'arguments dans votre fonction / méthode, plus les développeurs qui l'utilisent doivent comprendre. Cette charge cognitive n'est pas gratuite et si vous n'êtes pas cohérent avec l'ordre des arguments, etc., la charge cognitive ne fait qu'augmenter.

C'est un fait d'être humain. Je pense que l'erreur clé dans le livre Clean Code de l'oncle Bob est la déclaration "Le nombre idéal d'arguments pour une fonction est zéro" . Le minimalisme est formidable jusqu'à ce qu'il ne le soit pas. Tout comme vous n'atteignez jamais vos limites en calcul, vous n'atteindrez jamais le code "idéal" - vous ne devriez pas non plus.

Comme l'a dit Albert Einstein, «Tout devrait être aussi simple que possible, mais pas plus simple».

Deuxièmement: la leçon de David Arno

La façon de développer David Arno décrite est un développement de style plus fonctionnel qu'orienté objet . Cependant, le code fonctionnel évolue bien mieux que la programmation orientée objet traditionnelle. Pourquoi? À cause du verrouillage. Tout état temporel pouvant être modifié dans un objet, vous courez le risque de conditions de concurrence ou de conflit de verrouillage.

Ayant écrit des systèmes hautement concurrents utilisés dans des simulations et d'autres applications côté serveur, le modèle fonctionnel fait des merveilles. Je peux attester des améliorations apportées à l'approche. Cependant, c'est un style de développement très différent, avec des exigences et des idiomes différents.

Le développement est une série de compromis

Vous connaissez votre application mieux que nous. Vous n'avez peut-être pas besoin de l'évolutivité fournie avec la programmation de style fonctionnel. Il y a un monde entre les deux idéaux énumérés ci-dessus. Ceux d'entre nous qui traitent de systèmes qui doivent gérer un débit élevé et un parallélisme ridicule tendent vers l'idéal de la programmation fonctionnelle.

Cela dit, vous pouvez utiliser des objets de données pour contenir l'ensemble des informations dont vous avez besoin pour passer à une méthode. Cela aide à résoudre le problème de charge cognitive auquel oncle Bob s'attaquait, tout en soutenant l'idéal fonctionnel auquel David Arno s'attaquait.

J'ai travaillé sur les deux systèmes de bureau avec un parallélisme limité requis et sur un logiciel de simulation à haut débit. Ils ont des besoins très différents. Je peux apprécier un code orienté objet bien écrit qui est conçu autour du concept de masquage de données que vous connaissez. Cela fonctionne pour plusieurs applications. Cependant, cela ne fonctionne pas pour tous.

Qui a raison? Eh bien, David a plus raison que l'oncle Bob dans ce cas. Cependant, le point sous-jacent que je veux souligner ici est qu'une méthode devrait avoir autant d'arguments que de sens.

Berin Loritsch
la source
Il y a du paralélisme. Différentes batailles peuvent être traitées en parallèle. Cependant, oui: une seule bataille, en cours de traitement, doit être verrouillée.
gaazkam
Oui, je voulais dire que les lecteurs (moissonneurs dans votre analogie) glaneraient de leurs écrits (le semeur). Cela dit, je suis retourné pour regarder certaines choses que j'ai écrites dans le passé et j'ai soit réappris quelque chose ou été en désaccord avec mon ancien moi. Nous apprenons et évoluons tous, et c'est la raison numéro un pour laquelle vous devriez toujours raisonner sur la manière et si vous appliquez quelque chose que vous avez appris.
Berin Loritsch
8

OK - Je pourrais faire en sorte qu'une méthode RETOURNE un état de bataille au lieu de la modifier si je le voulais vraiment.

Oui, c'est l'idée.

Dois-je alors tout copier dans l'état de combat pour retourner un état entièrement nouveau au lieu de le modifier en place?

Non. Votre «état de bataille» pourrait être modélisé comme une structure de données immuable, qui contient d'autres structures de données immuables comme blocs de construction, peut-être imbriquées dans certaines hiérarchies de structures de données immuables.

Il pourrait donc y avoir des parties de l'état de bataille qui ne doivent pas être modifiées au cours d'un tour, et d'autres qui doivent être modifiées. Les parties qui ne changent pas ne doivent pas être copiées, car elles sont immuables, il suffit de copier une référence à ces parties, sans risque d'introduire des effets secondaires. Cela fonctionne mieux dans les environnements linguistiques récupérés.

Google pour "Efficient Immutable Data Structures", et vous trouverez sûrement quelques références sur la façon dont cela fonctionne en général.

Il me semble tellement plus simple de simplement modifier l'état en place et de renvoyer les mises à jour.

Pour certains problèmes, cela peut en effet être plus simple. Les jeux et les simulations basées sur les tours peuvent tomber dans cette catégorie, étant donné qu'une grande partie de l'état du jeu change d'un tour à l'autre. Cependant, la perception de ce qui est vraiment "plus simple" est dans une certaine mesure subjective, et cela dépend aussi beaucoup de ce à quoi les gens sont habitués.

Doc Brown
la source
8

En tant qu'auteur du commentaire, je suppose que je devrais le clarifier ici, car bien sûr, il y a plus que la version simplifiée que mon commentaire offre.

AFAIK ces deux principes sont fondamentalement incompatibles, proches d'être opposés l'un à l'autre: le pur fonctionnel peut être résumé comme "seulement des valeurs de retour, sans effets secondaires" tandis que dire, ne pas demander peut être résumé comme "ne retourne rien, avoir que des effets secondaires ".

Pour être honnête, je trouve que c'est une utilisation vraiment étrange du terme «dites, ne demandez pas». J'ai donc lu ce que Martin Fowler a dit à ce sujet il y a quelques années, ce qui était instructif . La raison pour laquelle j'ai trouvé cela étrange est que «ne demandez pas» est synonyme d'injection de dépendance dans ma tête et la forme la plus pure d'injection de dépendance passe tout ce dont une fonction a besoin via ses paramètres.

Mais il semble que le sens que j'appelle «dire ne demande pas» vient de la définition de Fowler axée sur l'OO et de la rendre plus paradigmatique agnostique. Dans le processus, je crois que cela amène le concept à ses conclusions logiques.

Revenons à des débuts simples. Nous avons des «morceaux de logique» (procédures) et nous avons des données globales. Les procédures lisent ces données directement pour y accéder. Nous avons un scénario simple de «demande».

Avancez un peu. Nous avons maintenant des objets et des méthodes. Ces données n'ont plus besoin d'être globales, elles peuvent être transmises via le constructeur et contenues dans l'objet. Et puis nous avons des méthodes qui agissent sur ces données. Alors maintenant, nous avons "dites, ne demandez pas" comme le décrit Fowler. L'objet est informé de ses données. Ces méthodes n'ont plus à demander l'étendue globale de leurs données. Mais voici le hic: ce n'est toujours pas vrai "dites, ne demandez pas" à mon avis, car ces méthodes doivent toujours demander la portée de l'objet. Il s'agit plus d'un scénario «dire, puis demander» que je ressens.

Alors, revenez aux temps modernes, videz l'approche «c'est OO tout le long» et empruntez certains principes à la programmation fonctionnelle. Désormais, lorsqu'une méthode est appelée, toutes les données lui sont fournies via ses paramètres. On peut (et a) fait valoir: "à quoi ça sert, ça ne fait que compliquer le code?" Et oui, la transmission via des paramètres, des données accessibles via la portée de l'objet, ajoute de la complexité au code. Mais le stockage de ces données dans un objet, plutôt que de le rendre globalement accessible, ajoute également de la complexité. Pourtant, rares sont ceux qui soutiennent que les variables globales sont toujours meilleures car elles sont plus simples. Le fait est que les avantages apportés par «dire, ne pas demander» l'emportent sur la complexité de la réduction de la portée. Cela s'applique davantage à la transmission via des paramètres qu'à la limitation de la portée à l'objet.private staticet passez tout ce dont il a besoin via les paramètres et maintenant cette méthode peut être fiable pour ne pas accéder sournoisement aux choses qu'elle ne devrait pas. De plus, cela encourage à garder la méthode petite, sinon la liste des paramètres devient incontrôlable. Et il encourage les méthodes d'écriture qui correspondent aux critères de «fonction pure».

Je ne vois donc pas «purement fonctionnel» et «dire, ne demandez pas» comme opposés. Le premier est la seule mise en œuvre complète du second en ce qui me concerne. L'approche de Fowler n'est pas complète "dites, ne demandez pas".

Mais il est important de se rappeler que cette «mise en œuvre complète de« ne demandez pas »est vraiment un idéal, c'est-à-dire que le pragmatisme doit entrer en jeu moins que nous devenons idéalistes et donc le traiter à tort comme la seule approche possible. Très peu d'applications peuvent devenir presque 100% sans effets secondaires pour la simple raison qu'elles ne feraient rien d'utile si elles étaient vraiment sans effets secondaires. Nous devons changer d'état, nous avons besoin d'E / S, etc. pour que l'application soit utile. Et dans de tels cas, les méthodes doivent provoquer des effets secondaires et ne peuvent donc pas être pures. Mais la règle d'or ici est de garder ces méthodes "impures" au minimum; seulement les avoir des effets secondaires parce qu'ils en ont besoin, plutôt que comme la norme.

Il me semble raisonnable d'avoir un état de bataille. Comme il s'agit d'un jeu au tour par tour, je garde les états de bataille dans un dictionnaire (multijoueur - il peut y avoir plusieurs batailles jouées par plusieurs joueurs en même temps). Chaque fois qu'un joueur fait son tour, j'appelle une méthode appropriée sur l'état de bataille qui (a) modifie l'état en conséquence et (b) renvoie des mises à jour aux joueurs, qui sont sérialisées en JSON et leur dis simplement ce qui vient de se passer sur le planche.

Il me semble plus que raisonnable d'avoir un état de bataille pour moi; cela semble essentiel. Le but de ce code est de gérer les demandes de changement d'état, de gérer ces changements d'état et de les signaler. Vous pouvez gérer cet état globalement, vous pouvez le maintenir à l'intérieur des objets d'un joueur individuel ou vous pouvez le passer autour d'un ensemble de fonctions pures. Celui que vous choisissez revient à celui qui fonctionne le mieux pour votre scénario particulier. L'état global simplifie la conception du code et est rapide, ce qui est une exigence clé de la plupart des jeux. Mais cela rend le code plus difficile à maintenir, à tester et à déboguer. Un ensemble de fonctions pures rendra le code plus complexe à implémenter et risque d'être trop lent en raison d'une copie excessive des données. Mais ce sera le plus simple à tester et à entretenir. L '"approche OO" se situe à mi-chemin entre les deux.

La clé est: il n'y a pas de solution parfaite qui fonctionne tout le temps. Le but des fonctions pures est de vous aider à "tomber dans le gouffre du succès". Mais si cette fosse est si peu profonde, en raison de la complexité qu'elle peut apporter au code, que vous n'y tombez pas tant que vous y trébuchez, alors ce n'est pas la bonne approche pour vous. Visez l'idéal, mais soyez pragmatique et arrêtez-vous lorsque cet idéal n'est pas un bon endroit où aller cette fois.

Et pour terminer, pour réitérer: les fonctions pures et «dire, ne pas demander» ne sont pas du tout opposées.

David Arno
la source
5

Pour quoi que ce soit, jamais dit, il existe un contexte, dans lequel vous pouvez mettre cette déclaration, qui la rendra absurde.

entrez la description de l'image ici

Oncle Bob a tout à fait tort si vous prenez le conseil de l'argument zéro comme une exigence. Il a tout à fait raison si vous considérez que chaque argument supplémentaire rend le code plus difficile à lire. Cela a un coût. Vous n'ajoutez pas d'arguments aux fonctions car cela les rend plus faciles à lire. Vous ajoutez des arguments aux fonctions parce que vous ne pouvez pas penser à un bon nom qui rende la dépendance à cet argument évidente.

Par exemple, pi()c'est une fonction parfaitement fine telle qu'elle est. Pourquoi? Parce que je me fiche de savoir comment, ou même si, il a été calculé. Ou s'il a utilisé e, ou sin (), pour arriver au nombre qu'il renvoie. Je suis d'accord avec ça parce que le nom me dit tout ce que j'ai besoin de savoir.

Cependant, tous les noms ne me disent pas tout ce que je dois savoir. Certains noms ne révèlent pas important de comprendre les informations qui contrôlent le comportement de la fonction ainsi que les arguments exposés. C'est exactement ce qui rend le style fonctionnel de programmation plus facile à raisonner.

Je peux garder les choses immuables et sans effets secondaires dans un style complètement OOP. Le retour est simplement un mécanisme utilisé pour laisser des valeurs sur la pile pour la procédure suivante. Vous pouvez rester tout aussi immuable en utilisant des ports de sortie pour communiquer des valeurs à d'autres choses immuables jusqu'à ce que vous atteigniez le dernier port de sortie qui doit finalement changer quelque chose si vous voulez que les gens puissent le lire. C'est vrai pour toutes les langues, fonctionnelles ou non.

Veuillez donc ne pas affirmer que la programmation fonctionnelle et la programmation orientée objet sont "fondamentalement incompatibles". Je peux utiliser des objets dans mes programmes fonctionnels et je peux utiliser des fonctions pures dans mes programmes OO.

Cependant, leur mélange a un coût: les attentes. Vous pouvez fidèlement suivre la mécanique des deux paradigmes et toujours semer la confusion. L'un des avantages de l'utilisation d'un langage fonctionnel est que les effets secondaires, bien qu'ils doivent exister pour obtenir une sortie, sont placés dans un endroit prévisible. À moins bien sûr qu'un objet mutable soit accessible de manière indisciplinée. Ensuite, ce que vous avez pris pour acquis dans cette langue s'effondre.

De même, vous pouvez prendre en charge des objets avec des fonctions pures, vous pouvez concevoir des objets qui sont immuables. Le problème est que si vous ne signalez pas que les fonctions sont pures ou que les objets sont immuables, les gens ne tirent aucun avantage de ces fonctionnalités tant qu'ils n'ont pas passé beaucoup de temps à lire le code.

Ce n'est pas un nouveau problème. Pendant des années, les gens ont codé de manière procédurale en "langues OO" en pensant qu'ils font OO parce qu'ils utilisent un "langage OO". Peu de langues sont si efficaces pour vous empêcher de vous tirer une balle dans le pied. Pour que ces idées fonctionnent, elles doivent vivre en vous.

Les deux offrent de bonnes fonctionnalités. Vous pouvez faire les deux. Si vous êtes assez courageux pour les mélanger, veuillez les étiqueter clairement.

candied_orange
la source
0

Je lutte parfois pour donner un sens à toutes les règles de divers paradigmes. Ils sont parfois en désaccord les uns avec les autres car ils sont dans cette situation.

La POO est un paradigme impératif qui consiste à courir avec des ciseaux dans le monde où des choses dangereuses se produisent.

FP est un paradigme fonctionnel dans lequel on trouve une sécurité absolue dans le calcul pur. Rien ne se passe ici.

Cependant, tous les programmes doivent faire le lien avec le monde impératif pour être utiles. Ainsi, noyau fonctionnel, coque impérative .

Les choses deviennent confuses lorsque vous commencez à définir des objets immuables (ceux dont les commandes renvoient une copie modifiée au lieu de réellement muter). Vous vous dites: «Ceci est la POO» et «Je définis le comportement des objets». Vous repensez au principe éprouvé Tell, Don't Ask. Le problème est que vous l'appliquez au mauvais domaine.

Les domaines sont entièrement différents et respectent des règles différentes. Le domaine fonctionnel se construit au point où il veut libérer des effets secondaires dans le monde. Pour que ces effets soient libérés, toutes les données qui auraient été encapsulées dans un objet impératif (si cela avait été écrit de cette façon!) Doivent être à la disposition du shell impératif. Sans accès à ces données qui, dans un autre monde, auraient été cachées par encapsulation, il ne peut pas faire le travail. C'est impossible sur le plan informatique.

Ainsi, lorsque vous écrivez des objets immuables (ce que Clojure appelle des structures de données persistantes), souvenez-vous que vous êtes dans le domaine fonctionnel. Jetez Tell, Don't Ask par la fenêtre et ne le laissez rentrer dans la maison que lorsque vous rentrez dans le royaume impératif.

Mario T. Lanza
la source