OK, donc le titre est un peu clickbaity mais sérieusement je suis sur un tell, ne demande pas un coup de pied pendant un moment. J'aime la façon dont cela encourage les méthodes à être utilisées comme messages de manière réellement orientée objet. Mais cela pose un problème récurrent qui me trotte dans la tête.
Je suis venu pour suspecter qu'un code bien écrit peut suivre les principes d'OO et les principes fonctionnels en même temps. J'essaie de réconcilier ces idées et le gros problème que j'ai rencontré est le suivant return
.
Une fonction pure a deux qualités:
L'appeler de manière répétée avec les mêmes entrées donne toujours le même résultat. Cela implique que c'est immuable. Son état n'est défini qu'une fois.
Il ne produit aucun effet secondaire. Le seul changement causé en l'appelant produit le résultat.
Alors, comment peut-on être purement fonctionnel si vous avez juré d'utiliser return
votre méthode de communication des résultats?
Le tell, ne demande pas idée fonctionne en utilisant ce que certains considèrent 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 que ça a fait. Je m'attends juste à ce qu'il ait fait quelque chose à propos de ce qu'il a été dit de faire.
Je pense à Tell, Don't Ask, à plus qu’un 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. Ce qui, dans de nombreux cas, est exprimé par l'état interne. Même si ce qui est exposé n’est pas exactement l’état, c’est généralement juste quelques calculs effectués sur des arguments d’état et d’entrée. Le fait de disposer d'une interface permettant de répondre offre la possibilité de transformer les résultats en quelque chose de plus significatif que l'état ou les calculs internes. C'est un message qui passe . Voir cet exemple .
Il y a bien longtemps, lorsque les disques durs contenaient des disques et qu'une clé USB correspondait à ce que vous faisiez dans la voiture lorsque le volant était trop froid pour être touché, on m'a appris à quel point les personnes agaçantes considèrent les fonctions sans paramètres. void swap(int *first, int *second)
semblait si pratique, mais nous avons été encouragés à écrire des fonctions renvoyant les résultats. J'ai donc pris cela à cœur avec foi et j'ai commencé à le suivre.
Mais maintenant, je vois des gens qui construisent des architectures où les objets laissent leur fabrication contrôler le lieu où ils envoient leurs résultats. Voici un exemple d'implémentation . L'injection de l'objet de port de sortie ressemble un peu à l'idée de paramètre out. Mais c'est comme ça que les objets qui disent ne pas demander disent aux autres objets ce qu'ils ont fait.
Lorsque j'ai découvert les effets secondaires, j'ai tout de suite pensé au paramètre de sortie. On nous disait de ne pas surprendre les gens en leur faisant effectuer une partie du travail de manière surprenante, c'est-à-dire en ne suivant pas la return result
convention. Maintenant, bien sûr, je sais qu’il existe une pile de problèmes de threads asynchrones parallèles qui entraînent des effets secondaires, mais le retour n’est en réalité qu’une convention selon laquelle vous laissez le résultat affiché sur la pile afin que tout ce qui est appelé puisse être supprimé ultérieurement. C'est tout ce que c'est vraiment.
Ce que j'essaie vraiment de demander:
Est-ce return
le seul moyen d'éviter toute cette misère d'effets secondaires et d'obtenir la sécurité du fil sans serrures, etc. Ou puis-je suivre , ne pas demander de manière purement fonctionnelle?
la source
Réponses:
Si une fonction n'a aucun effet secondaire et ne renvoie rien, alors la fonction est inutile. C'est aussi simple que ça.
Mais je suppose que vous pouvez utiliser certaines astuces si vous souhaitez suivre la lettre des règles et ignorer le raisonnement sous-jacent. Par exemple, utiliser un paramètre de sortie revient à ne pas utiliser de retour à proprement parler. Mais cela fait toujours exactement la même chose qu’un retour, mais d’une manière plus alambiquée. Donc, si vous croyez que le retour est mauvais pour une raison , utiliser un paramètre out est clairement mauvais pour les mêmes raisons sous-jacentes.
Vous pouvez utiliser des astuces plus compliquées. Par exemple, Haskell est célèbre pour le tour de la monade IO où vous pouvez avoir des effets secondaires dans la pratique, sans pour autant avoir d'effets secondaires d'un point de vue théorique. La poursuite du style est un autre truc, qui vous permet d’éviter les retours au prix de transformer votre code en spaghetti.
En fin de compte, sans astuces idiotes, les deux principes des fonctions sans effets secondaires et du "non-retour" ne sont tout simplement pas compatibles. En outre, je soulignerai que les deux principes sont vraiment mauvais (les dogmes en réalité), mais la discussion est différente.
Des règles telles que «Dites, ne demandez pas» ou «Aucun effet secondaire» 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 reconnaissent cela. Ils s'efforcent plutôt de séparer les parties pures du code de celles ayant des effets secondaires. L’intérêt des monades State ou IO dans Haskell n’est pas d’éviter les effets secondaires - parce que vous ne le pouvez pas - mais que la présence d’effets secondaires est explicitement indiquée par la signature de la fonction.
La règle tell-dont-ask s'applique à un type d'architecture différent - le style dans lequel les objets du programme sont des "acteurs" indépendants communiquant les uns avec les autres. 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 savoir si un message modifie l'état interne de l'acteur / de l'objet. L'état et les effets secondaires sont masqués par la conception .
la source
Dites, ne demandez pas vient avec quelques hypothèses fondamentales:
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. Cela peut se résumer comme ceci:
En d'autres termes, les classes sont seules responsables de maintenir leur propre état et d'agir en conséquence. C'est ce que l'encapsulation est tout.
De Fowler :
Pour répéter, rien de tout cela n'a à voir avec des fonctions pures, ni même impures, sauf si vous exposez l'état d'une classe au monde extérieur. Exemples:
Violation de la TDA
Pas une violation de la TDA
ou
Dans les deux derniers exemples, le feu de signalisation conserve le contrôle de son état et de ses actions.
la source
Vous ne demandez pas seulement son état interne , vous ne demandez pas non plus s'il y a un état interne .
Dites aussi , ne demandez pas! n'implique pas de ne pas obtenir un résultat sous la forme d'une valeur de retour (fournie par une
return
instruction dans la méthode). Cela implique simplement que je ne me soucie pas de la façon dont vous le faites, mais faites ce traitement! . Et parfois, vous voulez tout de suite le résultat des traitements ...la source
next()
méthode d' itérateurs ne doit pas seulement renvoyer l'objet actuel, mais également changer son état interne afin que l'appel suivant renvoie l'objet suivant ...Si vous considérez
return
comme "nuisible" (pour rester dans votre image), alors au lieu de faire une fonction commeconstruisez-le de manière à laisser passer les messages:
Tant que
f
etg
sont libres d'effets secondaires, les enchaînant sera effet secondaire libre aussi bien. Je pense que ce style est similaire à ce qu'on appelle aussi le style Continuation-passing .Si cela conduit vraiment à de "meilleurs" programmes, on peut en débattre, car cela rompt certaines conventions. L'ingénieur logiciel allemand Ralf Westphal a créé 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 des exemples, commencez par la section "Traduction en événements" de cette entrée de blog . Pour l’approche complète, je recommande son livre électronique "La messagerie en tant que modèle de programmation: faire de la POO comme si vous le vouliez" .
la source
If this really leads to "better" programs is debatable
Il suffit de regarder le code écrit en JavaScript au cours de la première décennie de ce siècle. Jquery et ses plugins étaient sujets aux callbacks de paradigmes ... callbacks partout . À un moment donné, trop de rappels imbriqués ont fait du débogage un cauchemar. Le code doit encore être lu par les humains quelles que soient les excentricités du génie logiciel et ses "principes"return
utiliser des données à partir de fonctions tout en adhérant à Tell Don't Ask, tant que vous ne contrôlez pas l'état d'un objet.Le passage de message est intrinsèquement efficace. Si vous demandez à 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'avez pas besoin de lui envoyer un message.
Dans les systèmes d'acteurs distribués, le résultat d'une opération est généralement envoyé sous forme de message à l' expéditeur de la demande d'origine. L'expéditeur du message est soit implicitement mis à disposition par le runtime de l'acteur, soit il est (par convention) explicitement passé en tant que partie du message. En mode de transmission synchrone, une réponse unique s'apparente à une
return
instruction. Lors de la transmission asynchrone de messages, 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 envoyé explicitement modélise fondamentalement le style de passage de continuation ou les paramètres redoutés - à l'exception du fait qu'il leur transmet des messages au lieu de les muter directement.
la source
Toute cette question me semble être une «violation de niveau».
Vous avez (au moins) les niveaux suivants dans un projet majeur:
Et ainsi de suite jusqu'aux jetons individuels.
Il n'est pas vraiment nécessaire qu'une entité au niveau de la méthode / fonction ne retourne pas (même si elle retourne simplement
this
). Et il n’est pas nécessaire (dans votre description) de demander à une entité au niveau acteur de restituer quoi que ce soit (en fonction du langage, cela peut même ne pas être possible). Je pense que la confusion provient de la fusion de ces deux niveaux, et je dirais qu'ils devraient faire l’objet d’un raisonnement distinct (même si un objet donné couvre en fait plusieurs niveaux).la source
Vous indiquez que vous souhaitez vous conformer à la fois 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 à fuir la déclaration.
Une façon relativement courante de suivre ces deux principes consiste à utiliser les déclarations de retour et à utiliser des objets immuables uniquement avec des accesseurs. L’approche consiste alors à demander à certains des getters de renvoyer un objet similaire avec un nouvel état, par opposition à une modification de l’état de l’objet original.
Un exemple de cette approche est fourni avec Python
tuple
etfrozenset
les types de données. Voici une utilisation typique d'un frozenset:Ce qui imprimera ce qui suit, démontrant que la méthode d'union crée un nouveau frozenset avec son propre état sans affecter les anciens objets:
Un autre exemple complet de structures de données immuables similaires est la bibliothèque Immutable.js de Facebook . Dans les deux cas, vous commencez avec ces blocs de construction et pouvez créer des objets de domaine de niveau supérieur qui suivent les mêmes principes, en obtenant une approche OOP fonctionnelle, qui vous aide à encapsuler les données et à les raisonner plus facilement. Et l'immuabilité vous permet également de tirer parti de la possibilité de partager de tels objets entre des threads sans vous soucier des verrous.
la source
J'ai essayé de faire de mon mieux pour concilier certains des avantages de la programmation impérative et fonctionnelle (bien sûr, je ne reçois pas tous les avantages, mais j'essaie d'obtenir la part du lion des deux), bien que ce
return
soit fondamental pour le faire en une méthode simple pour moi dans de nombreux cas.Pour ce qui est d’essayer d’éviter
return
carrément les déclarations, j’ai essayé de réfléchir à cela au cours des dernières heures et, en gros, la pile a débordé à plusieurs reprises dans mon cerveau. Je peux voir son intérêt en termes de renforcement du plus haut niveau d’encapsulation et d’informations se cachant en faveur d’objets très autonomes à qui il est simplement demandé ce qu’il faut faire, et j’aime explorer les extrémités des idées, ne serait-ce que pour essayer d’obtenir une meilleure compréhension de la façon dont ils travaillent.Si nous prenons l'exemple des feux de signalisation, une tentative naïve voudrait immédiatement lui donner une connaissance du monde entier qui l'entoure, ce qui serait certainement indésirable du point de vue du couplage. Donc, si je comprends bien, vous résumez cela et dissociez plutôt en faveur de la généralisation du concept de ports d'E / S qui propagent davantage les messages et les requêtes, et non les données, via le pipeline, et injectent essentiellement ces objets avec les interactions / requêtes souhaitées tout en oubliant les uns des autres.
Le pipeline nodal
Et ce diagramme est à peu près aussi long que j'ai essayé de le dessiner (et bien que simple, j'ai dû le changer et le repenser). Immédiatement, j'ai tendance à penser qu'un dessin avec ce niveau de découplage et d'abstraction trouverait son chemin devenant très difficile à raisonner sous forme de code, car le ou les orchestrateurs qui câblent toutes ces choses pour un monde complexe pourraient avoir beaucoup de difficulté à garder une trace de toutes ces interactions et demandes afin de créer le pipeline souhaité. Sous forme visuelle, toutefois, il peut être assez simple de simplement dessiner ces éléments sous forme de graphique, de tout relier et de voir les choses se dérouler de manière interactive.
En termes d’effets secondaires, j’ai pu constater que cela n’était pas dû à des "effets secondaires" dans le sens où ces demandes pouvaient, sur la pile d’appel, conduire à une chaîne de commandes pour chaque thread à exécuter, par exemple (je ne compte pas cela en tant qu '"effet secondaire" au sens pragmatique du fait que cela ne modifie aucun état pertinent pour le monde extérieur tant que de telles commandes ne sont pas exécutées - l'objectif pratique pour la plupart des logiciels est de ne pas éliminer les effets secondaires, mais de les différer et de les centraliser) . De plus, l'exécution de la commande peut générer un nouveau monde, par opposition à la mutation du monde existant. Mon cerveau est vraiment fatigué juste d'essayer de comprendre tout cela, cependant, en l'absence de toute tentative de prototyper ces idées. J'ai aussi pas
Comment ça marche
Donc, pour clarifier, j’imaginais comment vous programmez cela. La façon dont je le voyais fonctionner était en fait le diagramme ci-dessus qui capturait le flux de travail de l'utilisateur final (programmeur). Vous pouvez faire glisser un feu de signalisation dans le monde, faire glisser une minuterie, lui donner une période écoulée (après l'avoir "construite"). La minuterie a un
On Interval
événement (port de sortie), vous pouvez le connecter au feu afin que, sur de tels événements, il indique au feu de faire défiler ses couleurs.Le feu tricolore peut alors, lors du passage à certaines couleurs, émettre des sorties (événements) telles que,
On Red
à quel point nous pouvons traîner un piéton dans notre monde et faire en sorte que cet événement dise au piéton de commencer à marcher ... ou que nous puissions traîner des oiseaux 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 rien faire à part 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 à leur sujet (à moins que ces "événements" soient considérés comme des TMI, à ce moment-là, je devrai beaucoup repenser), ils se disent des choses à faire indirectement, ils ne le demandent pas. Et ils sont très 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 pratique?
Je pouvais voir ce genre de chose être utile comme langage intégré de haut niveau spécifique à un domaine dans certains domaines pour orchestrer tous ces objets autonomes qui ignorent tout du monde environnant, n'exposent rien de leur état interne post-construction et ne font que propager des requêtes entre nous, que nous pouvons changer et adapter au contenu de notre cœur. Pour le moment, j’ai l’impression que cela est très spécifique à un domaine, ou peut-être n’ai-je pas suffisamment réfléchi à la question, car il m’est très difficile de cerner le type de choses que je développe régulièrement (je travaille souvent avec code plutôt bas-moyen) si je devais interpréter Tell, Don't Ask à de telles extrémités et que je voulais le plus haut niveau d'encapsulation imaginable. Mais si nous travaillons avec des abstractions de haut niveau dans un domaine spécifique,
Signaux et Slots
Cette conception m’était étrangement familière jusqu’à ce que j’ai réalisé qu’il s’agissait essentiellement de signaux et de créneaux horaires si nous ne prenons pas beaucoup en compte les nuances de son implémentation. La principale question qui se pose à moi est de savoir avec quelle efficacité nous pouvons programmer ces nœuds individuels (objets) dans le graphe comme étant strictement conformes à Tell, Don't Ask, pris dans la mesure du possible
return
, et si nous pouvons évaluer ledit graphe sans mutations parallèle, par exemple absence de verrouillage). C'est là que les avantages magiques ne résident pas dans la manière dont nous connectons potentiellement ces éléments, mais dans la manière dont ils peuvent être mis en œuvre à ce degré d'encapsulation sans mutations. Ces deux solutions me semblent réalisables, mais je ne sais pas dans quelle mesure elles seraient applicables, et c’est pourquoi je suis un peu perplexe d’essayer de résoudre des cas d’utilisation potentiels.la source
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. En fonction de vos définitions (qui manquent en réalité dans le PO), des effets secondaires peuvent être totalement nécessaires (comme l'explique @JacquesB), ou sans aucune acceptation. Ou bien, faisant un pas de plus vers la clarification, il est nécessaire de faire la distinction entre les effets secondaires désirés que l’ on n’aime pas dissimuler (à ce stade, le célèbre IO de Haskell apparaît: ce n’est qu’un moyen d’être explicite) et les effets secondaires non désirés. résultat de bugs de code et ce genre de choses . Ce sont des problèmes assez différents et nécessitent donc un raisonnement différent.
Je suggère donc de commencer par vous reformuler: "Comment définissons-nous les effets secondaires et que disent les définitions à propos de son interrelation avec la déclaration" return "?".
la source