web-dev-qa-db-fra.com

Retour considéré comme nuisible? Le code peut-il être fonctionnel sans lui?

OK, donc le titre est un peu clickbaity mais sérieusement je suis sur un dis, ne demande pas coup de pied depuis un moment. J'aime la façon dont il encourage les méthodes à être utilisées comme messages de manière orientée objet. Mais cela a un problème tenace qui me tourbillonne dans la tête.

J'en suis venu à soupçonner qu'un code bien écrit peut suivre les principes OO et les principes fonctionnels en même temps. J'essaie de concilier ces idées et le gros point d'achoppement sur lequel j'ai atterri est return.

Une fonction pure a deux qualités:

  1. L'appeler à plusieurs reprises avec les mêmes entrées donne toujours le même résultat. Cela implique qu'il est immuable. Son état n'est défini qu'une seule fois.

  2. Il ne produit aucun effet secondaire. Le seul changement provoqué en l'appelant produit le résultat.

Alors, comment peut-on être purement fonctionnel si vous avez juré d'utiliser return comme moyen de communiquer les résultats?

L'idée dites, ne demandez pas fonctionne en utilisant ce que certains considéreraient comme un effet secondaire. Lorsque je traite un objet, je ne lui pose pas de question sur son état interne. Je lui dis ce que je dois faire et il utilise son état interne pour comprendre quoi faire avec ce que je lui ai dit de faire. Une fois que je le dis, je ne demande pas ce qu'il a fait. Je m'attends juste à ce qu'il ait fait quelque chose par rapport à ce qu'on lui avait dit de faire.

Je pense à Tell, Don't Ask comme plus qu'un simple nom différent pour l'encapsulation. Quand j'utilise return je n'ai aucune idée de ce qui m'a appelé. Je ne peux pas parler de protocole, je dois le forcer à gérer mon protocole. Qui dans de nombreux cas s'exprime comme l'état interne. Même si ce qui est exposé n'est pas exactement l'état, il s'agit généralement d'un calcul effectué sur les arguments d'état et d'entrée. Avoir une interface pour répondre à travers donne la possibilité de masser les résultats en quelque chose de plus significatif que l'état interne ou les calculs. C'est passage de message . Voir cet exemple .

Il y a longtemps, lorsque les disques durs contenaient des disques et qu'une clé USB était ce que vous faisiez dans la voiture lorsque la roue était trop froide pour toucher avec vos doigts, on m'a appris à quel point les gens ennuyeux considèrent les fonctions qui ont des paramètres. void swap(int *first, int *second) semblait si pratique mais nous avons été encouragés à écrire des fonctions qui retournaient les résultats. J'ai donc pris cela à cœur sur la foi et j'ai commencé à la suivre.

Mais maintenant, je vois des gens construire architectures où les objets laissent comment ils ont été construits contrôler où ils envoient leurs résultats. Voici un exemple d'implémentation . L'injection de l'objet du port de sortie semble un peu comme l'idée du paramètre de sortie. Mais c'est ainsi que les objets "ne demandez pas" disent aux autres objets ce qu'ils ont fait.

Quand j'ai découvert les effets secondaires pour la première fois, je l'ai pensé comme le paramètre de sortie. On nous disait de ne pas surprendre les gens en faisant en sorte que certains travaux se déroulent de manière surprenante, c'est-à-dire en ne suivant pas les return result convention. Maintenant, bien sûr, je sais qu'il y a une pile de problèmes de filetage asynchrone parallèle avec lesquels les effets secondaires se moquent, mais le retour n'est vraiment qu'une convention qui vous permet de laisser le résultat poussé sur la pile de sorte que tout ce que vous appelez puisse le faire disparaître plus tard. C'est tout ce que c'est vraiment.

Ce que j'essaie vraiment de demander:

return est-il le seul moyen d'éviter toute cette misère d'effets secondaires et d'obtenir la sécurité des threads sans verrous, etc. Ou puis-je suivre dites, ne demandez pas de manière purement fonctionnelle?

45
candied_orange

Si une fonction n'a pas d'effets secondaires et qu'elle ne renvoie rien, alors la fonction est inutile. C'est aussi simple que ça.

Mais je suppose que vous pouvez utiliser des astuces si vous voulez suivre la lettre des règles et ignorer le raisonnement sous-jacent. Par exemple, utiliser un paramètre out revient à strictement parler à ne pas utiliser de retour. Mais il fait toujours exactement la même chose qu'un retour, juste d'une manière plus compliquée. Donc, si vous pensez que le retour est mauvais pour une raison, alors utiliser un paramètre out est clairement mauvais pour les mêmes raisons sous-jacentes.

Vous pouvez utiliser des astuces plus alambiquées. Par exemple. Haskell est célèbre pour l'astuce IO monade où vous pouvez avoir des effets secondaires dans la pratique, mais toujours pas à proprement parler avoir des effets secondaires d'un point de vue théorique. Le style de passage-continu est une autre astuce, qui laisse bien vous évitez les retours au prix de transformer votre code en spaghetti.

En fin de compte, en l'absence d'astuces idiotes, les deux principes des fonctions libres d'effets secondaires et du "pas de retour" ne sont tout simplement pas compatibles. De plus, je soulignerai que les deux sont en fait de très mauvais principes (des dogmes vraiment), mais c'est une discussion différente.

Les règles comme "dites, ne demandez pas" ou "pas d'effets secondaires" ne peuvent pas être appliquées universellement. Vous devez toujours tenir compte du contexte. Un programme sans effets secondaires est littéralement inutile. Même les langages fonctionnels purs le reconnaissent. Ils s'efforcent plutôt de séparer les parties pures du code de celles qui ont des effets secondaires. Le point de l'état ou IO monades dans Haskell n'est pas que vous évitez les effets secondaires - parce que vous ne pouvez pas - mais que la présence d'effets secondaires est indiqué explicitement par la signature de fonction.

La règle de ne pas demander s'applique à un autre type d'architecture - le style où les objets du programme sont des "acteurs" indépendants communiquant entre eux. Chaque acteur est fondamentalement autonome et encapsulé. Vous pouvez lui envoyer un message et il décide comment y réagir, mais vous ne pouvez pas examiner l'état interne de l'acteur de l'extérieur. Cela signifie que vous ne pouvez pas dire si un message change l'état interne de l'acteur/objet. Les états et les effets secondaires sont masqués par conception.

96
JacquesB

Dites, ne demandez pas est livré avec quelques hypothèses fondamentales:

  1. Vous utilisez des objets.
  2. Vos objets ont un état.
  3. L'état de vos objets affecte leur comportement.

Aucune de ces choses ne s'applique aux fonctions pures.

Voyons donc pourquoi nous avons la règle "Dites, ne demandez pas". Cette règle est un avertissement et un rappel. Il peut être résumé comme ceci:

Permettez à votre classe de gérer son propre état. Ne lui demandez pas son état, puis agissez en fonction de cet état. Dites à la classe ce que vous voulez et laissez-la décider quoi faire en fonction de son propre état.

En d'autres termes, les classes sont seules responsables du maintien de leur propre état et de son action. C'est à cela que sert l'encapsulation.

De Fowler :

Tell-Don't-Ask est un principe qui aide les gens à se rappeler que l'orientation objet consiste à regrouper des données avec les fonctions qui opèrent sur ces données. Cela nous rappelle que plutôt que de demander à un objet des données et d'agir sur ces données, nous devrions plutôt dire à un objet quoi faire. Cela nous encourage à déplacer le comportement dans un objet pour aller avec les données.

Pour le répéter, rien de tout cela n'a rien à voir avec des fonctions pures, ou même impures, sauf si vous exposez l'état d'une classe au monde extérieur. Exemples:

Violation TDA

var color = trafficLight.Color;
var elapsed = trafficLight.Elapsed;
If (color == Color.Red && elapsed > 2.Minutes)
    trafficLight.ChangeColor(green);

Pas une violation TDA

var result = trafficLight.ChangeColor(Color.Green);

ou

var result = await trafficLight.ChangeColorWhenReady(Color.Green);     

Dans les deux derniers exemples, le feu de circulation conserve le contrôle de son état et de ses actions.

32
Robert Harvey

Lorsque je traite un objet, je ne lui pose pas de question sur son état interne. Je lui dis ce que je dois faire et il utilise son état interne pour comprendre quoi faire avec ce que je lui ai dit de faire.

Vous ne demandez pas seulement son état interne , vous ne demandez pas s'il a un état interne à tous non plus.

Aussi dites, ne demandez pas! ne pas implique de ne pas obtenir un résultat sous la forme d'une valeur de retour (fournie par un return dans la méthode). Cela implique simplement Je me fiche de comment vous le faites, mais faites ce traitement! . Et parfois, vous voulez immédiatement le résultat des traitements ...

30
Timothy Truckle

Si vous considérez return comme "nuisible" (pour rester dans votre image), alors au lieu de faire une fonction comme

ResultType f(InputType inputValue)
{
     // ...
     return result;
}

construisez-le de manière à transmettre les messages:

void f(InputType inputValue, Action<ResultType> g)
{
     // ...
     g(result);
}

Tant que f et g sont sans effet secondaire, les enchaîner ensemble sera également sans effet secondaire. Je pense que ce style est similaire à ce qu'on appelle aussi style de passage contin .

Si cela conduit vraiment à de "meilleurs" programmes, c'est discutable, car cela casse certaines conventions. L'ingénieur logiciel allemand Ralf Westphal a fait tout un modèle de programmation autour de cela, il l'a appelé "Event Based Components" avec une technique de modélisation qu'il appelle "Flow Design".

Pour voir quelques exemples, commencez dans la section "Traduction en événements" de cette entrée de blog . Pour l'approche complète, je recommande son e-book "Messaging as a Programming model - Doing OOP as if you mean it") .

17
Doc Brown

La transmission de messages est intrinsèquement efficace. Si vous dites à un objet de faire quelque chose, vous vous attendez à ce qu'il ait un effet sur quelque chose. Si le gestionnaire de messages était pur, vous n'auriez pas besoin de lui envoyer de message.

Dans les systèmes d'acteurs distribués, le résultat d'une opération est généralement renvoyé sous forme de message à l'expéditeur de la demande d'origine. L'expéditeur du message est soit implicitement mis à disposition par le moteur d'exécution de l'acteur, soit il est (par convention) explicitement transmis en tant que partie du message. Lors de la transmission de messages synchrones, une seule réponse s'apparente à une instruction return. Lors du passage de messages asynchrones, l'utilisation de messages de réponse est particulièrement utile car elle permet un traitement simultané dans plusieurs acteurs tout en fournissant des résultats.

Le passage de l '"expéditeur" auquel le résultat doit être livré explicitement modélise essentiellement style de passage de continuation ou les paramètres redoutés - sauf qu'il leur transmet des messages au lieu de les muter directement.

7
Bergi

Toute cette question me semble être une "violation de niveau".

Vous avez (au moins) les niveaux suivants dans un grand projet:

  • Le niveau du système, par ex. plateforme e-commerce
  • Le niveau du sous-système, par ex. validation utilisateur: serveur, AD, frontal
  • Le niveau du programme individuel, par ex. l'un des composants ci-dessus
  • Le niveau Acteur/Module [cela devient trouble selon la langue]
  • Le niveau méthode/fonction.

Et ainsi de suite jusqu'aux jetons individuels.

Il n'y a pas vraiment besoin d'une entité au niveau méthode/fonction pour ne pas retourner (même si elle retourne juste this). Et il n'y a pas (dans votre description) la nécessité pour une entité au niveau de l'acteur de renvoyer quoi que ce soit (selon la langue qui n'est peut-être même pas possible). Je pense que la confusion réside dans la fusion de ces deux niveaux, et je dirais qu'ils devraient être raisonnés de manière distincte (même si un objet donné s'étend sur plusieurs niveaux).

5
Jared Smith

Vous mentionnez que vous voulez vous conformer au principe OOP de "dire, ne demandez pas" et au principe fonctionnel des fonctions pures, mais je ne vois pas très bien comment cela vous a conduit à évitez la déclaration de retour.

Une manière alternative relativement courante de suivre ces deux principes est de faire des all-in sur les instructions de retour et d'utiliser des objets immuables avec des getters uniquement. L'approche consiste alors à demander à certains des getters de renvoyer un objet similaire avec un nouvel état, au lieu de changer l'état de l'objet d'origine.

Un exemple de cette approche est dans les types de données Python builtin Tuple et frozenset. Voici une utilisation typique d'un ensemble de frozensets:

small_digits = frozenset([0, 1, 2, 3, 4])
big_digits = frozenset([5, 6, 7, 8, 9])
all_digits = small_digits.union(big_digits)

print("small:", small_digits)
print("big:", big_digits)
print("all:", all_digits)

Ce qui imprimera ce qui suit, démontrant que la méthode d'union crée un nouvel ensemble de frozens avec son propre état sans affecter les anciens objets:

petit: frozenset ({0, 1, 2, 3, 4})

grand: frozenset ({5, 6, 7, 8, 9})

tous: frozenset ({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

La bibliothèque Immutable.js de Facebook est un autre exemple étendu de structures de données immuables similaires. Dans les deux cas, vous commencez par ces blocs de construction et pouvez créer des objets de domaine de niveau supérieur qui suivent les mêmes principes, réalisant une approche fonctionnelle OOP, qui vous aide à encapsuler les données et à les raisonner plus facilement. Et l'immuabilité vous permet également de profiter de la possibilité de partager de tels objets entre les threads sans avoir à vous soucier des verrous.

2
yoniLavi

J'en suis venu à soupçonner qu'un code bien écrit peut suivre les principes OO et les principes fonctionnels en même temps. J'essaie de concilier ces idées et le gros point d'achoppement sur lequel j'ai atterri c'est le retour.

J'ai fait de mon mieux pour concilier certains des avantages, plus spécifiquement, de la programmation impérative et fonctionnelle (naturellement, ne tirant pas parti de tous les avantages, mais essayant d'obtenir la part du lion des deux), bien que return soit en fait fondamental pour le faire de manière simple pour moi dans de nombreux cas.

En ce qui concerne la tentative d'éviter purement et simplement les déclarations de return, j'ai essayé de réfléchir à cela pendant environ une heure et j'ai essentiellement empilé mon cerveau plusieurs fois. Je peux voir son attrait en termes d'imposition du plus fort niveau d'encapsulation et de dissimulation d'informations au profit d'objets très autonomes qui ne font que dire quoi faire, et j'aime explorer les extrémités des idées, ne serait-ce que pour essayer d'obtenir une meilleure comprendre comment ils fonctionnent.

Si nous utilisons l'exemple des feux de circulation, alors immédiatement une tentative naïve voudrait donner une telle connaissance des feux de circulation du monde entier qui l'entoure, et ce serait certainement indésirable du point de vue du couplage. Donc, si je comprends bien, vous résumez cela et vous vous dissociez en faveur de la généralisation du concept de ports d'E/S qui propagent davantage les messages et les demandes, pas les données, via le pipeline, et injectez essentiellement ces objets avec les interactions/demandes souhaitées entre eux tout en étant inconscients les uns des autres.

Le pipeline nodal

enter image description here

Et ce diagramme est à peu près aussi loin que j'essayais de l'esquisser (et bien que simple, j'ai dû continuer à le changer et à le repenser). Immédiatement, j'ai tendance à penser qu'une conception avec ce niveau de découplage et d'abstraction trouverait très difficile de raisonner sous forme de code, car les orchestrateurs qui câblent toutes ces choses pour un monde complexe pourraient trouver très difficile garder une trace de toutes ces interactions et demandes afin de créer le pipeline souhaité. Sous une forme visuelle, cependant, il pourrait être raisonnablement simple de simplement dessiner ces choses sous forme de graphique et de tout relier et de voir les choses se produire de manière interactive.

En termes d'effets secondaires, je pouvais voir que cela était exempt d '"effets secondaires" dans le sens où ces demandes pouvaient, sur la pile d'appels, conduire à une chaîne de commandes pour chaque thread à effectuer, par ex. (Je ne considère pas cela comme un "effet secondaire" dans un sens pragmatique car il ne modifie aucun état pertinent pour le monde extérieur jusqu'à ce que de telles commandes soient réellement exécutées - le but pratique pour moi dans la plupart des logiciels n'est pas d'éliminer le côté effets mais les reporter et les centraliser). De plus, l'exécution de la commande pourrait produire un nouveau monde au lieu de muter celui existant. Mon cerveau est vraiment taxé juste en essayant de comprendre tout cela cependant, en l'absence de toute tentative de prototypage de ces idées. Je n'ai pas non plus essayé d'aborder la façon de passer les paramètres avec les demandes en faveur de simplement essayer une approche timide au début de penser à toutes ces demandes comme des fonctions nulles avec une signature/interface uniforme.

Comment ça marche

Donc, pour clarifier, j'imaginais comment vous programmez réellement cela. La façon dont je le voyais fonctionner était en fait le diagramme ci-dessus capturant le flux de travail de l'utilisateur (programmeur). Vous pouvez faire glisser un feu de circulation dans le monde, faire glisser une minuterie, lui donner une période de temps écoulée (lors de sa "construction"). La minuterie a un On Interval événement (port de sortie), vous pouvez le connecter au feu de circulation afin que sur de tels événements, il indique à la lumière de parcourir ses couleurs.

Le feu de circulation peut alors, en passant à certaines couleurs, émettre des sorties (événements) comme, On Red, à quel point nous pourrions faire glisser un piéton dans notre monde et faire en sorte que cet événement dise au piéton de commencer à marcher ... ou nous pourrions faire glisser des oiseaux dans notre scène et faire en sorte que lorsque la lumière devient rouge, nous disons aux oiseaux de commencer voler et battre des ailes ... ou peut-être lorsque la lumière devient rouge, nous disons à une bombe d'exploser - tout ce que nous voulons, et avec les objets complètement inconscients les uns des autres, et ne faisant rien d'autre que de se dire indirectement quoi faire à travers ce concept abstrait d'entrée/sortie.

Et ils encapsulent pleinement leur état et ne révèlent rien à ce sujet (à moins que ces "événements" ne soient considérés comme TMI, auquel cas je devrais beaucoup repenser les choses), ils se disent des choses à faire indirectement, ils ne demandent pas. Et ils sont super découplés. Rien ne sait rien d'autre que cette abstraction généralisée des ports d'entrée/sortie.

Cas d'utilisation pratiques?

Je pouvais voir ce type de chose utile en tant que langage intégré de haut niveau spécifique à un domaine dans certains domaines pour orchestrer tous ces objets autonomes qui ne connaissent rien du monde environnant, n'exposent rien de leur construction interne de post-état et ne font que propager des requêtes entre nous que nous pouvons changer et Tweak au contenu de nos cœurs. Pour le moment, j'ai l'impression que cela est très spécifique au domaine, ou peut-être que je n'y ai pas suffisamment réfléchi, car il est très difficile pour moi de me concentrer sur le type de choses que je développe régulièrement (je travaille souvent avec code plutôt bas-milieu) si je devais interpréter Tell, Don't Ask à de telles extrémités et que je souhaite le niveau d'encapsulation le plus fort imaginable. Mais si nous travaillons avec des abstractions de haut niveau dans un domaine spécifique, cela pourrait être un moyen très utile de le programmer et d'exprimer comment les choses interagissent les unes avec les autres d'une manière plutôt uniforme qui ne se confond pas dans l'état, ou des calculs/sorties, de ses objets, avec un découplage uber d'une sorte où même l'appelant analogique n'a pas besoin de savoir grand-chose, le cas échéant, sur son appelé, ou vice versa.

Signaux et emplacements

Cette conception me semblait étrangement familière jusqu'à ce que je réalise que ce sont essentiellement des signaux et des slots si nous ne prenons pas beaucoup en compte les nuances de la façon dont elle est mise en œuvre. La principale question pour moi est de savoir dans quelle mesure nous pouvons programmer ces nœuds individuels (objets) dans le graphique en respectant strictement Tell, Don't Ask, dans la mesure où ils évitent les instructions return, et si nous pouvons évaluer ledit graphique sans mutations (en parallèle, par exemple, sans verrouillage). C'est là que les avantages magiques ne résident pas dans la façon dont nous connectons ces choses ensemble, mais dans la façon dont elles peuvent être mises en œuvre à ce degré d'encapsulation en l'absence de mutations. Les deux me semblent réalisables, mais je ne sais pas dans quelle mesure ce serait largement applicable, et c'est là que je suis un peu perplexe en essayant de travailler sur des cas d'utilisation potentiels.

1
Dragon Energy

Je vois clairement une fuite de certitude ici. Il semble que "effet secondaire" soit un terme bien connu et communément compris, mais en réalité il ne l'est pas. Selon vos définitions (qui manquent réellement dans le PO), les effets secondaires peuvent être totalement nécessaires (comme @JacquesB a réussi à l'expliquer), ou impitoyablement refusés. Ou, en faisant un pas vers la clarification, il est nécessaire de faire la distinction entre souhaité les effets secondaires que l'on n'aime pas cacher (à ce stade, le célèbre IO émerge de Haskell: ce n'est rien d'autre qu'un moyen d'être explicite) et indésirable les effets secondaires en tant que résultat de bogues de code et ce genre de choses . Ce sont des problèmes assez différents et nécessitent donc un raisonnement différent.

Donc, je suggère de commencer par reformuler: "Comment définissons-nous l'effet secondaire et que disent les définitions données à propos de son interrelation avec la déclaration de" retour "?".

0
Sereja Bogolubov