Nebenwirkungen, die die referenzielle Transparenz beeinträchtigen

11

Die funktionale Programmierung in Scala erklärt die Auswirkungen eines Nebeneffekts auf die Aufhebung der referenziellen Transparenz:

Nebeneffekt, der eine Verletzung der referenziellen Transparenz impliziert.

Ich habe einen Teil von SICP gelesen , in dem die Verwendung des „Substitutionsmodells“ zur Bewertung eines Programms erörtert wird.

Da ich das Substitutionsmodell mit referentieller Transparenz (RT) grob verstehe, können Sie eine Funktion in ihre einfachsten Teile zerlegen. Wenn der Ausdruck RT ist, können Sie den Ausdruck zerlegen und immer das gleiche Ergebnis erhalten.

Wie im obigen Zitat angegeben, kann / wird die Verwendung von Nebenwirkungen das Substitutionsmodell jedoch zerstören.

Beispiel:

val x = foo(50) + bar(10)

Wenn foound bar keine Nebenwirkungen auftreten, gibt die Ausführung einer der beiden Funktionen immer das gleiche Ergebnis zurück x. Wenn sie jedoch Nebenwirkungen haben, ändern sie eine Variable, die das Substitutionsmodell stört.

Ich fühle mich mit dieser Erklärung wohl, aber ich verstehe sie nicht ganz.

Bitte korrigieren Sie mich und füllen Sie alle Lücken in Bezug auf Nebenwirkungen aus, die die RT brechen, und besprechen Sie auch die Auswirkungen auf das Substitutionsmodell.

Kevin Meredith
quelle

Antworten:

20

Beginnen wir mit einer Definition für referenzielle Transparenz :

Ein Ausdruck wird als referenziell transparent bezeichnet, wenn er durch seinen Wert ersetzt werden kann, ohne das Verhalten eines Programms zu ändern (mit anderen Worten, ein Programm zu erhalten, das dieselben Effekte und Ausgaben bei derselben Eingabe hat).

Das bedeutet, dass Sie (zum Beispiel) in jedem Teil des Programms 2 + 5 durch 7 ersetzen können und das Programm weiterhin funktionieren sollte. Dieser Vorgang wird als Substitution bezeichnet. Die Ersetzung ist nur dann gültig, wenn 2 + 5 durch 7 ersetzt werden kann, ohne dass dies Auswirkungen auf einen anderen Teil des Programms hat .

Nehmen wir an, ich habe eine Klasse Bazmit den Funktionen Foound Bardarin. Der Einfachheit halber sagen wir das einfach Foound Barbeide geben den übergebenen Wert zurück. Also Foo(2) + Bar(5) == 7, wie Sie es erwarten würden. Die referenzielle Transparenz garantiert, dass Sie Foo(2) + Bar(5)den Ausdruck an einer 7beliebigen Stelle in Ihrem Programm durch den Ausdruck ersetzen können und das Programm weiterhin identisch funktioniert.

Was aber, wenn Fooder übergebene Wert zurückgegeben wird, aber Barder übergebene Wert plus der zuletzt angegebene Wert zurückgegeben wird Foo? Dies ist einfach genug, wenn Sie den Wert von Fooin einer lokalen Variablen innerhalb der BazKlasse speichern . Wenn der Anfangswert dieser lokalen Variablen 0 ist, gibt der Ausdruck Foo(2) + Bar(5)den erwarteten Wert des 7ersten Aufrufs zurück, aber 9das zweite Mal, wenn Sie ihn aufrufen.

Dies verletzt die referenzielle Transparenz auf zwei Arten. Erstens kann nicht davon ausgegangen werden, dass Bar bei jedem Aufruf denselben Ausdruck zurückgibt. Zweitens ist ein Nebeneffekt aufgetreten, nämlich dass das Aufrufen von Foo den Rückgabewert von Bar beeinflusst. Da Sie nicht mehr garantieren können, dass dies Foo(2) + Bar(5)7 entspricht, können Sie nicht mehr ersetzen.

Dies ist, was referentielle Transparenz in der Praxis bedeutet; Eine referenziell transparente Funktion akzeptiert einen bestimmten Wert und gibt einen entsprechenden Wert zurück, ohne anderen Code an anderer Stelle im Programm zu beeinflussen. Bei gleicher Eingabe wird immer dieselbe Ausgabe zurückgegeben.

Robert Harvey
quelle
5
RTWenn Sie also brechen, können Sie das nicht verwenden. substitution model.Das große Problem , wenn Sie das nicht verwenden können, substitution modelist die Fähigkeit, es zu verwenden, um über ein Programm nachzudenken?
Kevin Meredith
Das ist genau richtig.
Robert Harvey
1
+1 wunderbar klare und verständliche Antwort. Vielen Dank.
Racheet
2
Auch wenn diese Funktionen transparent oder "rein" sind, ist die Reihenfolge, in der sie tatsächlich ausgeführt werden, nicht wichtig. Es ist uns egal, ob foo () oder bar () zuerst ausgeführt werden, und in einigen Fällen werden sie möglicherweise nie ausgewertet, wenn sie nicht benötigt werden
Zachary K
1
Ein weiterer Vorteil von RT besteht darin, dass teure referenziell transparente Ausdrücke zwischengespeichert werden können (da eine ein- oder zweimalige Auswertung genau das gleiche Ergebnis liefern sollte).
Dcastro
3

Stellen Sie sich vor, Sie versuchen eine Mauer zu bauen und erhalten eine Auswahl an Kisten in verschiedenen Größen und Formen. Sie müssen ein bestimmtes L-förmiges Loch in die Wand füllen. Sollten Sie nach einer L-förmigen Box suchen oder können Sie zwei gerade Boxen der entsprechenden Größe ersetzen?

In der Funktionswelt lautet die Antwort, dass beide Lösungen funktionieren. Wenn Sie Ihre funktionale Welt aufbauen, müssen Sie niemals die Kisten öffnen, um zu sehen, was sich darin befindet.

In der imperativen Welt ist es gefährlich, eine Wand zu bauen, ohne den Inhalt jeder Schachtel zu überprüfen und ihn mit dem Inhalt jeder anderen Schachtel zu vergleichen:

  • Einige enthalten starke Magnete und drücken andere Magnetboxen aus der Wand, wenn sie nicht richtig ausgerichtet sind.
  • Einige sind sehr heiß oder kalt und reagieren schlecht, wenn sie in angrenzenden Räumen aufgestellt werden.

Ich denke, ich werde aufhören, bevor ich Ihre Zeit mit unwahrscheinlicheren Metaphern verschwende, aber ich hoffe, dass der Punkt gemacht wird; Funktionsbausteine ​​enthalten keine versteckten Überraschungen und sind vollständig vorhersehbar. Da Sie immer kleinere Blöcke mit der richtigen Größe und Form verwenden können, um einen größeren zu ersetzen, und es keinen Unterschied zwischen zwei Feldern mit derselben Größe und Form gibt, haben Sie referenzielle Transparenz. Bei zwingenden Ziegeln reicht es nicht aus, etwas in der richtigen Größe und Form zu haben - man muss wissen, wie der Ziegel gebaut wurde. Nicht referenziell transparent.

In einer reinen funktionalen Sprache müssen Sie lediglich die Signatur einer Funktion sehen, um zu wissen, was sie tut. Natürlich könnten Sie nach innen schauen , wie gut es führt zu sehen, aber Sie nicht haben zu sehen.

In einer imperativen Sprache weiß man nie, welche Überraschungen sich darin verbergen könnten.

itsbruce
quelle
"In einer reinen funktionalen Sprache müssen Sie nur die Signatur einer Funktion sehen, um zu wissen, was sie tut." - Das stimmt im Allgemeinen nicht. Ja, unter der Annahme eines parametrischen Polymorphismus können wir schließen, dass eine Funktion des Typs (a, b) -> anur die fstFunktion sein kann und dass eine Funktion des Typs a -> anur die identityFunktion sein kann, aber Sie können beispielsweise nicht unbedingt etwas über eine Funktion des Typs sagen (a, a) -> a.
Jörg W Mittag
2

Da ich das Substitutionsmodell (mit referentieller Transparenz (RT)) grob verstehe, können Sie eine Funktion in ihre einfachsten Teile zerlegen. Wenn der Ausdruck RT ist, können Sie den Ausdruck zerlegen und immer das gleiche Ergebnis erhalten.

Ja, die Intuition ist ganz richtig. Hier sind einige Hinweise, um genauer zu werden:

Wie Sie sagten, sollte jeder RT-Ausdruck ein single"Ergebnis" haben. Das heißt, wenn ein factorial(5)Ausdruck im Programm gegeben ist, sollte er immer das gleiche "Ergebnis" liefern. Wenn also ein bestimmtes factorial(5)Element im Programm enthalten ist und 120 ergibt, sollte es immer 120 ergeben, unabhängig davon, welche "Schrittreihenfolge" es erweitert / berechnet - unabhängig von der Zeit .

Beispiel: die factorialFunktion.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Bei dieser Erklärung gibt es einige Überlegungen.

Beachten Sie zunächst, dass die verschiedenen Bewertungsmodelle (siehe anwendbare vs. normale Reihenfolge) unterschiedliche "Ergebnisse" für dieselbe RT-Expression liefern können.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

Im obigen Code firstund secondsind referenziell transparent, und dennoch liefert der Ausdruck am Ende unterschiedliche "Ergebnisse", wenn er in normaler und anwendbarer Reihenfolge ausgewertet wird (unter letzterer hört der Ausdruck nicht auf).

.... was zur Verwendung von "Ergebnis" in Anführungszeichen führt. Da ein Ausdruck nicht angehalten werden muss, wird möglicherweise kein Wert erzeugt. Die Verwendung von "Ergebnis" ist also etwas verschwommen. Man kann sagen, dass ein RT-Ausdruck computationsunter einem Bewertungsmodell immer dasselbe ergibt .

Drittens kann es erforderlich sein, zwei foo(50)im Programm an verschiedenen Stellen als unterschiedliche Ausdrücke zu sehen - jeder liefert seine eigenen Ergebnisse, die sich voneinander unterscheiden können. Wenn die Sprache beispielsweise einen dynamischen Bereich zulässt, sind beide Ausdrücke unterschiedlich, obwohl sie lexikalisch identisch sind. In Perl:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

Der dynamische Bereich führt in die Irre, weil er es einem leicht macht zu denken, dass dies xder einzige Input ist foo, wenn es in Wirklichkeit so ist xund y. Eine Möglichkeit, den Unterschied zu erkennen, besteht darin, das Programm in ein äquivalentes Programm ohne dynamischen Bereich umzuwandeln, dh die Parameter explizit zu übergeben. Anstatt zu definieren foo(x), definieren foo(x, y)und übergeben wir ydie Aufrufer explizit.

Der Punkt ist, wir sind immer unter einer functionDenkweise: Wenn wir eine bestimmte Eingabe für einen Ausdruck erhalten, erhalten wir ein entsprechendes "Ergebnis". Wenn wir den gleichen Input geben, sollten wir immer das gleiche "Ergebnis" erwarten.

Was ist nun mit dem folgenden Code?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

Die fooProzedur bricht RT, weil es Neudefinitionen gibt. Das heißt, wir haben yin einem Punkt definiert und später dasselbe neu definiert y. Im obigen Perl-Beispiel sind die ys unterschiedliche Bindungen, obwohl sie denselben Buchstabennamen "y" haben. Hier sind die ys eigentlich gleich. Deshalb sagen wir, dass (Neu-) Zuweisung eine Metaoperation ist: Sie ändern tatsächlich die Definition Ihres Programms.

In der Regel wird der Unterschied wie folgt dargestellt: In einer Umgebung ohne Nebenwirkungen haben Sie eine Zuordnung von input -> output. In einer "imperativen" Einstellung haben Sie input -> ouputim Kontext eine state, die sich im Laufe der Zeit ändern kann.

Anstatt nur die entsprechenden Werte durch Ausdrücke zu ersetzen, müssen statebei jeder Operation, die dies erfordert , Transformationen auf die angewendet werden (und natürlich können Ausdrücke diese konsultieren, stateum Berechnungen durchzuführen).

Wenn also in einem nebenwirkungsfreien Programm alles, was wir wissen müssen, um einen Ausdruck zu berechnen, seine individuelle Eingabe ist, müssen wir in einem imperativen Programm die Eingaben und den gesamten Zustand für jeden Rechenschritt kennen. Das Denken ist das erste, das einen großen Schlag erleidet (um ein problematisches Verfahren zu debuggen, benötigen Sie die Eingabe und den Core-Dump). Bestimmte Tricks werden unpraktisch gemacht, wie das Auswendiglernen. Parallelität und Parallelität werden jedoch auch viel schwieriger.

Thiago Silva
quelle
1
Schön, dass Sie Memoization erwähnen. Dies kann als Beispiel für einen internen Zustand verwendet werden, der außerhalb nicht sichtbar ist: Eine Funktion, die Memoization verwendet, ist immer noch referenziell transparent, obwohl sie intern Status und Mutation verwendet.
Giorgio