OK, der Titel ist ein bisschen knifflig, aber im Ernst, ich war auf einem Tell, frag eine Weile nicht nach Kick. Mir gefällt, wie es Methoden anregt, wirklich objektorientiert als Botschaften verwendet zu werden . Aber das hat ein quälendes Problem, das mir durch den Kopf ging.
Ich habe den Verdacht, dass gut geschriebener Code gleichzeitig OO-Prinzipien und Funktionsprinzipien folgen kann. Ich versuche, diese Ideen in Einklang zu bringen, und der große Knackpunkt, auf den ich gestoßen bin, ist return
.
Eine reine Funktion hat zwei Eigenschaften:
Ein wiederholter Aufruf mit denselben Eingaben führt immer zum selben Ergebnis. Dies impliziert, dass es unveränderlich ist. Sein Zustand wird nur einmal gesetzt.
Es entstehen keine Nebenwirkungen. Die einzige durch den Aufruf verursachte Änderung ist das Erzeugen des Ergebnisses.
Wie kann man also rein funktional sein, wenn man darauf verzichtet, return
Ergebnisse zu kommunizieren?
Die Tell-Don't-Ask- Idee funktioniert unter Verwendung dessen, was einige als Nebenwirkung betrachten würden. Wenn ich mich mit einem Objekt befasse, frage ich es nicht nach seinem internen Zustand. Ich sage ihm, was ich tun muss, und er verwendet seinen internen Zustand, um herauszufinden, was mit dem zu tun ist, was ich ihm gesagt habe. Sobald ich es erzähle, frage ich nicht, was es getan hat. Ich erwarte nur, dass es etwas getan hat, was ihm befohlen wurde.
Ich denke an Tell, Ask nicht mehr als nur einen anderen Namen für die Verkapselung. Wenn ich benutze, habe return
ich keine Ahnung, wie ich genannt wurde. Ich kann nicht sagen, dass es sich um ein Protokoll handelt, ich muss es erzwingen, um mit meinem Protokoll fertig zu werden. Was in vielen Fällen als interner Zustand ausgedrückt wird. Auch wenn das, was belichtet wird, nicht genau der Status ist, handelt es sich in der Regel nur um eine Berechnung, die für Status- und Eingabeargumente ausgeführt wird. Eine Schnittstelle zu haben, über die man reagieren kann, bietet die Möglichkeit, die Ergebnisse in einen aussagekräftigeren Zustand als interne Zustände oder Berechnungen zu überführen. Das ist die Weitergabe von Nachrichten . Siehe dieses Beispiel .
Vor langer Zeit, als Festplatten tatsächlich Festplatten enthielten und ein USB-Stick das war, was Sie im Auto taten, als das Rad zu kalt war, um es mit den Fingern zu berühren, wurde mir beigebracht, wie nervig Menschen Funktionen betrachten, die über Parameter verfügen. void swap(int *first, int *second)
schien so praktisch, aber wir wurden ermutigt, Funktionen zu schreiben, die die Ergebnisse zurückgaben. Also nahm ich mir den Glauben zu Herzen und fing an, ihm zu folgen.
Aber jetzt sehe ich Leute, die Architekturen bauen, in denen Objekte ihre Konstruktionsweise kontrollieren lassen, wohin sie ihre Ergebnisse senden. Hier ist eine Beispielimplementierung . Das Injizieren des Output-Port-Objekts scheint wieder ein bisschen wie die Out-Parameter-Idee zu sein. Aber so sagen Sie anderen Objekten, was sie getan haben.
Als ich zum ersten Mal etwas über Nebenwirkungen erfuhr, stellte ich mir das als Ausgabeparameter vor. Wir sollten die Leute nicht überraschen, indem wir einen Teil der Arbeit auf überraschende Weise erledigen, dh indem wir uns nicht an die return result
Konvention halten. Nun sicher, ich weiß, dass es eine Menge paralleler asynchroner Threading-Probleme gibt, mit denen sich die Nebenwirkungen herumschlagen, aber Return ist eigentlich nur eine Konvention, bei der Sie das Ergebnis auf dem Stapel belassen, damit Sie es später entfernen können. Das ist alles was es wirklich ist.
Was ich wirklich zu fragen versuche:
Ist return
die einzige Möglichkeit, all diese Nebenwirkungen zu vermeiden und die Sicherheit des Fadens ohne Sperren usw. zu gewährleisten ? Oder kann ich sagen, nicht auf rein funktionale Weise fragen ?
quelle
Antworten:
Wenn eine Funktion keine Nebenwirkungen hat und nichts zurückgibt, ist sie unbrauchbar. So einfach ist das.
Aber ich denke, Sie können einige Cheats verwenden, wenn Sie den Buchstaben der Regeln befolgen und die zugrunde liegende Argumentation ignorieren möchten . Wenn Sie beispielsweise einen out-Parameter verwenden, wird streng genommen kein return verwendet. Aber es tut genau das Gleiche wie eine Rückkehr, nur auf eine verschlungenere Art und Weise. Wenn Sie also glauben, dass die Rückgabe aus einem bestimmten Grund schlecht ist , ist die Verwendung eines out-Parameters aus denselben Gründen eindeutig schlecht.
Sie können mehr verschlungene Cheats verwenden. ZB ist Haskell berühmt für den IO-Monadentrick, bei dem Sie in der Praxis Nebenwirkungen haben können, aber theoretisch noch nicht unbedingt Nebenwirkungen haben. Continuation-Passing-Stil ist ein weiterer Trick, mit dem Sie Renditen vermeiden können, wenn Sie Ihren Code in Spaghetti verwandeln.
Das Fazit ist, dass ohne alberne Tricks die beiden Prinzipien der nebenwirkungsfreien Funktionen und "keine Rendite" einfach nicht kompatibel sind. Außerdem möchte ich darauf hinweisen, dass es sich bei beiden um wirklich schlechte Prinzipien (Dogmen) handelt, aber das ist eine andere Diskussion.
Regeln wie "Tell, Don't Ask" oder "No Side Effects" können nicht universell angewendet werden. Sie müssen immer den Kontext berücksichtigen. Ein Programm ohne Nebenwirkungen ist buchstäblich nutzlos. Auch reine Funktionssprachen erkennen das an. Sie bemühen sich vielmehr, die reinen Teile des Codes von denjenigen mit Nebenwirkungen zu trennen . Der Punkt der State- oder IO-Monaden in Haskell ist nicht, dass Sie Nebenwirkungen vermeiden - weil Sie dies nicht können -, sondern dass das Vorhandensein von Nebenwirkungen explizit durch die Funktionssignatur angezeigt wird.
Die Tell-Dont-Ask-Regel gilt für eine andere Art von Architektur - den Stil, bei dem Objekte im Programm unabhängige "Akteure" sind, die miteinander kommunizieren. Jeder Akteur ist grundsätzlich autonom und gekapselt. Sie können ihm eine Nachricht senden und er entscheidet, wie er darauf reagieren soll, aber Sie können den internen Zustand des Akteurs nicht von außen untersuchen. Dies bedeutet, dass Sie nicht feststellen können, ob eine Nachricht den internen Status des Akteurs / Objekts ändert. Zustand und Nebenwirkungen werden durch das Design ausgeblendet .
quelle
Tell, Don't Ask beinhaltet einige grundlegende Annahmen:
Nichts davon gilt für reine Funktionen.
Sehen wir uns also an, warum wir die Regel "Tell, Don't Ask" haben. Diese Regel ist eine Warnung und eine Erinnerung. Es kann wie folgt zusammengefasst werden:
Anders ausgedrückt, die Klassen sind allein dafür verantwortlich, ihren eigenen Zustand aufrechtzuerhalten und danach zu handeln. Darum geht es bei der Kapselung.
Von Fowler :
Um es noch einmal zu wiederholen: Nichts davon hat etwas mit reinen oder sogar unreinen Funktionen zu tun, es sei denn, Sie setzen den Zustand einer Klasse der Außenwelt aus. Beispiele:
TDA-Verstoß
Keine TDA-Verletzung
oder
In beiden letzteren Beispielen behält die Ampel die Kontrolle über ihren Zustand und ihre Aktionen.
quelle
Sie fragen nicht nur nach dem internen Status , sondern auch, ob er überhaupt einen internen Status hat .
Auch sagen, fragen Sie nicht! bedeutet nicht , kein Ergebnis in Form eines Rückgabewerts zu erhalten (bereitgestellt durch eine
return
Anweisung innerhalb der Methode). Es bedeutet nur, dass es mir egal ist, wie Sie es machen, aber machen Sie diese Verarbeitung! . Und manchmal will man sofort das Bearbeitungsergebnis ...quelle
next()
Methode sollte nicht nur das aktuelle Objekt zurückgeben, sondern auch den internen Status des Iterators ändern, sodass der nächste Aufruf das nächste Objekt zurückgibt ...Wenn Sie
return
als "schädlich" betrachten (um in Ihrem Bild zu bleiben), dann machen Sie stattdessen eine Funktion wieBauen Sie es in einer Message-Passing-Weise:
Solange
f
undg
nebenwirkungsfrei sind, sind Verkettungen auch nebenwirkungsfrei. Ich denke, dieser Stil ähnelt dem, was man auch als Continuation-Passing-Stil bezeichnet .Ob dies wirklich zu "besseren" Programmen führt, ist umstritten, da es einige Konventionen verletzt. Der deutsche Softwareentwickler Ralf Westphal hat ein ganzes Programmiermodell erstellt, er nannte es "Event Based Components" mit einer Modellierungstechnik, die er "Flow Design" nennt.
Beginnen Sie im Abschnitt "In Ereignisse übersetzen" dieses Blogeintrags , um einige Beispiele anzuzeigen . Für den vollständigen Ansatz empfehle ich sein E-Book "Messaging als Programmiermodell - OOP so tun, als ob Sie es gemeint hätten" .
quelle
If this really leads to "better" programs is debatable
Wir müssen uns nur den Code ansehen, der im ersten Jahrzehnt dieses Jahrhunderts in JavaScript geschrieben wurde. JQuery und seine Plugins waren anfällig für dieses Paradigma Rückrufe ... Rückrufe überall . Zu viele verschachtelte Rückrufe machten das Debuggen zu einem Albtraum. Code muss immer noch von Menschen gelesen werden, ungeachtet der Exzentrizität des Software-Engineerings und seiner "Prinzipien"return
Daten von Funktionen verwenden und sich trotzdem an Tell Don't Ask halten, solange Sie nicht den Status eines Objekts befehlen.Die Weitergabe von Nachrichten ist von Natur aus effektiv. Wenn Sie sagen , ein Objekt zu tun , etwas, erwarten Sie es eine Wirkung auf etwas haben. Wenn der Nachrichtenhandler rein wäre, müssten Sie ihm keine Nachricht senden.
In Systemen mit verteilten Akteuren wird das Ergebnis einer Operation normalerweise als Nachricht an den Absender der ursprünglichen Anforderung zurückgesendet. Der Absender der Nachricht wird entweder implizit von der Actor-Laufzeit zur Verfügung gestellt oder (gemäß Konvention) explizit als Teil der Nachricht übergeben. Bei der synchronen Nachrichtenübergabe entspricht eine einzelne Antwort einer
return
Anweisung. Bei der asynchronen Nachrichtenübergabe ist die Verwendung von Antwortnachrichten besonders nützlich, da sie die gleichzeitige Verarbeitung in mehreren Akteuren ermöglicht und dennoch Ergebnisse liefert.Die Übergabe des "Absenders", an den das Ergebnis explizit übermittelt werden soll, modelliert im Grunde den Fortsetzungs-Übergabestil oder die gefürchteten Parameter - mit der Ausnahme, dass Nachrichten an sie übergeben werden, anstatt sie direkt zu mutieren.
quelle
Diese ganze Frage erscheint mir als "Level-Verletzung".
Sie haben (mindestens) folgende Stufen in einem Großprojekt:
Und so weiter bis zu einzelnen Token.
Es ist nicht wirklich erforderlich, dass eine Entität auf Methoden- / Funktionsebene nicht zurückgibt (auch wenn sie nur zurückgibt
this
). Und es gibt (in Ihrer Beschreibung) keine Notwendigkeit, dass eine Entität auf der Ebene des Akteurs etwas zurückgibt (abhängig von der Sprache, die möglicherweise nicht einmal möglich ist). Ich denke, die Verwirrung besteht darin, diese beiden Ebenen miteinander zu verschmelzen, und ich würde argumentieren, dass sie eindeutig begründet werden sollten (selbst wenn ein bestimmtes Objekt tatsächlich mehrere Ebenen umfasst).quelle
Sie erwähnen, dass Sie sowohl dem OOP-Prinzip "Tell, Don't Ask" als auch dem Funktionsprinzip reiner Funktionen entsprechen möchten, aber ich verstehe nicht ganz, wie Sie dies veranlasst haben, die return-Anweisung zu umgehen.
Eine relativ gebräuchliche Alternative, um diesen beiden Prinzipien zu folgen, besteht darin, die return-Anweisungen zu all-in und unveränderliche Objekte nur mit Getern zu verwenden. Der Ansatz ist dann, dass einige der Getter ein ähnliches Objekt mit einem neuen Status zurückgeben, anstatt den Status des ursprünglichen Objekts zu ändern.
Ein Beispiel für diesen Ansatz sind die integrierten Python-
tuple
undfrozenset
Datentypen. Hier ist eine typische Verwendung eines Frozensets:Mit dem folgenden Ausdruck wird demonstriert, dass die Vereinigungsmethode ein neues Frozenset mit eigenem Status erstellt, ohne die alten Objekte zu beeinflussen:
Ein weiteres umfangreiches Beispiel für ähnliche unveränderliche Datenstrukturen ist die Bibliothek Immutable.js von Facebook . In beiden Fällen beginnen Sie mit diesen Bausteinen und können übergeordnete Domänenobjekte erstellen, die den gleichen Grundsätzen folgen. Auf diese Weise erhalten Sie einen funktionalen OOP-Ansatz, mit dem Sie die Daten und die Gründe dafür einfacher kapseln können. Durch die Unveränderlichkeit können Sie auch den Vorteil nutzen, solche Objekte zwischen Threads gemeinsam nutzen zu können, ohne sich um Sperren kümmern zu müssen.
quelle
Ich habe mein Bestes gegeben, um einige der Vorteile der imperativen und funktionalen Programmierung in Einklang zu bringen (natürlich nicht alle Vorteile, aber ich versuche, den Löwenanteil von beiden zu bekommen), obwohl dies
return
eigentlich von grundlegender Bedeutung ist in vielen fällen eine einfache art und weise für mich.Um
return
Aussagen zu vermeiden , habe ich in der letzten Stunde versucht, darüber nachzudenken, und im Grunde ist mein Gehirn mehrmals überfüllt. Ich sehe die Anziehungskraft davon darin, die stärkste Ebene der Verkapselung und des Versteckens von Informationen zugunsten von sehr autonomen Objekten zu erzwingen, denen lediglich gesagt wird, was zu tun ist, und ich mag es, die äußersten Grenzen von Ideen zu erkunden, wenn ich nur versuche, eine Verbesserung zu erreichen Verständnis dafür, wie sie funktionieren.Wenn wir das Ampel-Beispiel verwenden, dann würde ein naiver Versuch sofort ein solches Ampel-Wissen über die gesamte Welt, die es umgibt, vermitteln wollen, und das wäre sicherlich aus Kopplungsperspektive unerwünscht. Wenn ich das richtig verstehe, abstrahieren Sie dies und entkoppeln sich, um das Konzept der E / A-Ports zu verallgemeinern, die Nachrichten und Anforderungen, nicht Daten, durch die Pipeline weiterleiten, und fügen im Grunde genommen die gewünschten Interaktionen / Anforderungen untereinander in diese Objekte ein während sie sich nicht bewusst sind.
Die Knotenpipeline
Und dieses Diagramm ist ungefähr so weit wie ich versucht habe, es zu skizzieren (und obwohl es einfach ist, musste ich es immer wieder ändern und neu überdenken). Ich neige dazu zu glauben, dass ein Design mit dieser Ebene der Entkopplung und Abstraktion es sehr schwierig finden würde, in Codeform darüber nachzudenken, weil es für die Orchestratoren, die all diese Dinge für eine komplexe Welt verkabeln, sehr schwierig sein könnte Behalten Sie alle diese Interaktionen und Anforderungen im Auge, um die gewünschte Pipeline zu erstellen. In visueller Form mag es jedoch recht einfach sein, diese Dinge einfach als Diagramm zu zeichnen und alles miteinander zu verknüpfen und zu sehen, wie Dinge interaktiv geschehen.
In Bezug auf die Nebenwirkungen konnte ich feststellen, dass dies in dem Sinne frei von "Nebenwirkungen" ist, dass diese Anforderungen auf dem Aufrufstapel zu einer Befehlskette für jeden auszuführenden Thread führen können, z. B. (Ich zähle dies nicht als "Nebeneffekt" im pragmatischen Sinne, da es keinen für die Außenwelt relevanten Zustand verändert, bis solche Befehle tatsächlich ausgeführt werden - das praktische Ziel für mich in den meisten Programmen ist es nicht, Nebeneffekte zu beseitigen, sondern sie zu verschieben und zu zentralisieren) . Und außerdem könnte die Befehlsausführung eine neue Welt ausgeben, anstatt die existierende zu mutieren. Mein Gehirn ist wirklich belastet, wenn es nur versucht, all dies zu verstehen, ohne einen Versuch zu unternehmen, diese Ideen zu prototypisieren. Ich habe auch nicht
Wie es funktioniert
Um das zu verdeutlichen, habe ich mir vorgestellt, wie Sie das tatsächlich programmieren. Die Art und Weise, wie ich es funktionierte, war das obige Diagramm, das den Workflow für Benutzer (Programmierer) aufzeichnete. Sie können eine Ampel in die Welt ziehen, eine Zeitschaltuhr ziehen und eine verstrichene Zeitspanne festlegen (wenn Sie sie "konstruieren"). Der Timer verfügt über ein
On Interval
Ereignis (Ausgangsport), das Sie an die Ampel anschließen können, damit die Ampel bei solchen Ereignissen durch ihre Farben wechselt.Die Ampel kann dann beim Umschalten auf bestimmte Farben Ausgaben (Ereignisse) ausgeben, wie zum Beispiel,
On Red
an welchem Punkt wir einen Fußgänger in unsere Welt ziehen und dieses Ereignis den Fußgänger anweisen, mit dem Gehen zu beginnen ... oder wir könnten Vögel hineinziehen Wenn das Licht rot wird, sagen wir den Vögeln, sie sollen fliegen und mit den Flügeln schlagen. Oder wenn das Licht rot wird, sagen wir einer Bombe, sie soll explodieren - was immer wir wollen und die Objekte sollen explodieren einander völlig vergessen und nichts anderes tun, als sich indirekt mitzuteilen, was durch dieses abstrakte Eingabe- / Ausgabekonzept zu tun ist.Und sie kapseln ihren Zustand vollständig ein und verraten nichts darüber (es sei denn, diese "Ereignisse" werden als TMI betrachtet, an diesem Punkt müsste ich viel überdenken), sie sagen einander, dass sie indirekt etwas tun sollen, sie fragen nicht. Und sie sind überaus entkoppelt. Nichts weiß etwas anderes als diese verallgemeinerte Eingabe / Ausgabe-Port-Abstraktion.
Praktische Anwendungsfälle?
Ich könnte diese Art von Dingen als eine domänenspezifische eingebettete Sprache auf hoher Ebene in bestimmten Domänen sehen, um all diese autonomen Objekte zu orchestrieren, die nichts über die umgebende Welt wissen, nichts über ihren internen Zustand nach der Konstruktion preisgeben und im Grunde nur Anfragen verbreiten untereinander, die wir nach Herzenslust verändern und optimieren können. Im Moment denke ich, dass dies sehr domänenspezifisch ist, oder ich habe einfach nicht genug darüber nachgedacht, weil es für mich sehr schwierig ist, mein Gehirn mit den Dingen zu beschäftigen, mit denen ich mich regelmäßig entwickle (ich arbeite oft damit) eher Low-Mid-Level-Code), wenn ich Tell, Don't Ask zu solchen Extremitäten interpretieren und den stärksten Grad der Kapselung, den man sich vorstellen kann, wünschen würde. Aber wenn wir mit Abstraktionen auf hoher Ebene in einer bestimmten Domäne arbeiten,
Signale und Slots
Dieses Design kam mir merkwürdig bekannt vor, bis mir klar wurde, dass es sich im Grunde genommen um Signale und Slots handelt, wenn wir nicht die Nuancen berücksichtigen, wie es implementiert wird. Die Hauptfrage für mich ist, wie effektiv wir diese einzelnen Knoten (Objekte) im Graphen so programmieren können, dass sie sich strikt an Tell, Don't Ask halten, bis zum Grad der Vermeidung von
return
Aussagen, und ob wir den Graphen ohne Mutationen auswerten können (in parallel, zB fehlende Verriegelung). Hier liegt der magische Vorteil nicht darin, wie wir diese Dinge potenziell miteinander verbinden, sondern wie sie ohne Mutationen in diesem Grad der Verkapselung implementiert werden können. Beides scheint mir machbar zu sein, aber ich bin mir nicht sicher, wie weit verbreitet es sein würde, und da bin ich ein bisschen ratlos, wenn ich versuche, mögliche Anwendungsfälle durchzuarbeiten.quelle
Ich sehe hier ein deutliches Sicherheitsleck. Es scheint, dass "Nebeneffekt" ein bekannter und allgemein verständlicher Begriff ist, aber in Wirklichkeit ist dies nicht der Fall. Abhängig von Ihren Definitionen (die im OP tatsächlich fehlen) können Nebenwirkungen (wie von @JacquesB erklärt) völlig notwendig oder gnadenlos inakzeptabel sein. Oder, um einen Schritt in Richtung Klärung zu machen, muss man zwischen erwünschten Nebenwirkungen, die man nicht gerne versteckt (an diesen Stellen taucht das berühmte Haskell-IO auf: Es ist nichts anderes als eine Möglichkeit, explizit zu sein) und unerwünschten Nebenwirkungen als unterscheiden Ergebnis von Code-Fehlern und solchen Dingen . Das sind ziemlich unterschiedliche Probleme und erfordern daher unterschiedliche Überlegungen.
Also schlage ich vor, mit dem Umformulieren zu beginnen: "Wie definieren wir Nebenwirkungen und was sagen bestimmte Definitionen über die Wechselbeziehung mit der" return "-Anweisung aus?"
quelle