Hallo Felix
function f1(x) {
return y => x * y;
}
const f5 = f1(5);
f5(7);
Man kann auch sehen, dass das zurückgegebene Funktionsobjekt die Variable x
"kennt"
Das kann man sehen, weil JavaScript statischen Scope besitzt – und JavaScript besitzt statischen Scope, damit man sowas sehen kann. ;-)
Schauen wir uns einmal an, wie Julias Beispiel in Perl aussehen könnte:
sub f1($x) {
return -> $y {$x * $y}
}
my &f5 = f1(5);
&f5(7); # 35
Ich denke, die Syntax ist ähnlich genug, so dass es keiner allzu großen Erklärungen bedarf. Wir deklarieren mit sub
eine gewöhnliche Funktion mit einem Parameter. Das Sigil $
vor Parameter- bzw. Variablennamen zeigt an, dass ein skalarer Wert gespeichert werden soll. Wie in der JavaScript-Vorlage geben wir eine anonyme Funktion zurück, und zwar mit einer gebundenen Variable y
und einer freien Variable x
. (Das Schlüsselwort return
hätten wir uns dabei eigentlich sparen können, da ohne eine explizite Anweisung der Wert des letzten Ausdrucks zurückgegben wird.)
my &f5 = f1(5);
Hier rufen wir f1
auf und speichern die zurückgegebene anonyme Funktion in einer Variable. Das Sigil &
vor dem Variablennamen zeigt an, dass ein Objekt referenziert werden soll, das aufgerufen werden kann. Im Anschluss an die obige Anweisung und analog zur Vorlage rufen wir die in f5
hinterlegte Funktion dann mit dem Wert 7
auf. Da Perl ebenso wie JavaScript standardmäßig die Regeln für statischen bzw. lexikalischen Scope anwendet, wird hier auch dasselbe Ergebnis zurückgegeben:
&f5(7); # 35
Im Gegensatz zu den allermeisten anderen Programmiersprachen besteht in Perl aber die Möglichkeit, andere Regeln für die Sichtbarkeit von Variablen festzulegen. Wird zwischen Sigil und Bezeichner einer Variablen ein *
notiert, dann werden auf diese Variable die Regeln für dynamischen Scope angewendet. Schauen wir uns am Ausgangsbeispiel an, welche Konsequenzen das hat:
my $*x = 4;
sub f1($*x) {
return -> $y {$*x * $y}
}
my &f5 = f1(5);
Hier haben wir an drei Stellen Veränderungen vorgenommen. Wir haben im globalen Gültigkeitsbereich nun eine Variable x
mit dem Wert 4, deren Sichtbarkeit nicht statisch, sondern dynamisch bestimmt wird. Außerdem haben wir sowohl in der Parameterliste von f1
als auch innerhalb der anonymen Funktion, die von f1
zurückgegeben wird, für x
zwischen Sigil und Bezeichner ein *
notiert. Wie schon im ersten Beispiel oben rufen wir f1
mit dem Wert 5 auf und speichern das Ergebnis in der Variable f5
. Wenn wir diese Funktion nun wie oben mit dem Wert 7 aufrufen, kommt folgendes Ergebnis dabei heraus:
my &f5 = f1(5);
&f5(7); # 28
Ups. Was ist hier passiert? :-)
Offenbar hat die innerhalb von f1
erzeugte und in der Variable f5
gespeicherte Funktion den Parameter x
aus dem lokalen Scope der Funktion f1
ignoriert und bei der Auswertung des arithmetischen Ausdrucks stattdessen die globale Variable x
referenziert!
my $*x = 4;
Anders als bei statischem Scope, wird bei dynamischem Scope die Sichtbarkeit eines Bezeichners erst zur Laufzeit des Programms bestimmt. Der Gültigkeitsbereich von Variablen ist hier in erster Linie nicht räumlich, sondern zeitlich definiert. Ob also eine Bindung für einen Bezeichner existiert – und falls ja, welcher Wert mit diesem Bezeichner verknüpft ist, hängt vom Zeitpunkt der Programmausführung ab, das heißt davon, welche Funktionen in welcher Reihenfolge aufgerufen wurden.
&f5(7); # 28
Wir rufen die in f5
hinterlegte Funktion aus dem globalen Scope auf und übergeben dabei den Wert 7, der an den einzigen Parameter y
gebunden wird. Dann passiert zunächst das gleiche, was auch schon vorher passiert ist: Es wird festgestellt, dass innerhalb der Funktion f5
keine Bindung für den Bezeichner x
existiert. Nun wird jedoch nicht in der lexikalischen Umgebung, in der f5
definiert wurde, nach x
gesucht, sondern in dem Gültigkeitsbereich der mit dem aufrufenden Kontext verknüpft ist, in diesem Fall also im globalen Scope. Dort haben wir eine Variable x
definiert und folglich wird der Wert dieser Variable in den Ausdruck eingesetzt.
Betrachten wir noch ein weiteres Beispiel, um den Ablauf zu verdeutlichen:
# Global execution context
my $*x = 10;
# Create local variable and call other function
sub f {
my $*x = 20;
g();
}
# Add then print to stdout
sub g {
say $*x += 10;
}
Wie im letzten Beispiel haben wir hier zunächst eine globale Variable namens x
definiert. Dazu kommen zwei Funktionen, f
und g
, die beide keine Argumente erwarten. (In diesem Fall können in Perl die Klammern für die Parameterliste weggelassen werden.) Die Funktion f
definiert eine lokale Variable x
und ruft die Funktion g
auf. Diese wiederum greift auf ein x
zu und addiert 10, bevor sie den aktuellen Wert von x
ausgibt.
Was passiert nun, wenn wir f
aufrufen? Welcher Wert wird ausgegeben?
+--------------+
| global |
| |
| x = 10 |
+--------------+
Wenn wir das Programm starten, wird ein Stack Frame für den globalen Ausführungskontext auf den Call Stack gelegt, der wichtige Informationen über unser Programm enthält, unter anderem auch die Information, welche Variablen im globalen Gültigkeitsbereich definiert sind. Dazu gehört hier auch die Variable x
, die gleich zu Anfang im globalen Scope deklariert und mit dem Wert 10 initialisiert wurde.
+--------------+
| f |
| |
| x = 20 |
+--------------+
| global |
| |
| x = 10 |
+--------------+
Rufen wir die Funktion f
auf, dann wird für diesen Aufruf ein weiterer Rahmen auf den Stapel gelegt. Es wird vermerkt, dass im Körper der Funktion f
eine lokale Variable namens x
deklariert wurde, deren Wert 20 ist.
+--------------+
| g |
| |
| |
+--------------+
| f |
| |
| x = 20 |
+--------------+
| global |
| |
| x = 10 |
+--------------+
Aus f
heraus wird g
aufgerufen, und auch für diesen Aufruf wird ein Stack Frame auf den Stapel gelegt. In der Funktion g
wird keine lokale Variable angelegt, aber dafür auf eine Variable x
zugegriffen.
my $*x = 10;
# snip
sub g {
say $*x += 10;
}
Wenn wir diesen Teil des Codes betrachten, scheint klar, welches x
in g
referenziert wird: g
scheint das globale x
zu „kennen“. Da für x
jedoch die Regeln für dynamischen Scope gelten, spielt die Tatsache keine Rolle, dass die Funktion g
selbst im globalen Gültigkeitsbereich deklariert wurde. Statt dort nach einer Bindung für x
zu suchen, wird der Gültigkeitsbereich durchsucht, der mit dem vorletzten Frame auf dem Stack assoziiert ist, also der lokale Scope der Funktion f
, aus der heraus g
aufgerufen wurde:
+--------------+
| g |
| |
| | x += 10
+--------------+
| f | ▲
| | |
| x = 20 | ___|
+--------------+
| global |
| |
| x = 10 |
+--------------+
Da innerhalb von f
eine Bindung für den gesuchten Bezeichner existiert, wird das x
aus diesem Gültigkeitsbereich referenziert. Es werden 10 addiert und im Ergebnis wird der Wert 30 ausgegeben – und nicht 20, was der Fall gewesen wäre, wenn die globale Variable mit dem selben Namen referenziert worden wäre. Die globale Variable wäre nur dann erreicht worden, wenn im Scope von f
keine Bindung für x
vorliegen würde.
+--------------+
| f |
| |
| x = 30 |
+--------------+
| global |
| |
| x = 10 |
+--------------+
Nach dem der Aufruf von g
abgearbeitet wurde, wird der dazugehörige Frame vom Stack wieder entfernt. Würde innerhalb von f
weiter mit x
gearbeitet, dann mit dessen neuem Wert 30. Andere Funktionen, die gegebenenfalls aus diesem Kontext heraus noch aufgerufen werden, würden entsprechend dieselbe Variable referenzieren. Es sei denn, es würde bei einem der Aufrufe selbst wieder eine lokale Variable mit dem Namen x
deklariert, welche die Variablen in f
und im globalen Scope verschattet.
+--------------+
| global |
| |
| x = 10 |
+--------------+
Dies gilt in gleicher Weise für den globalen Ausführungskontext, wenn f
terminiert und der zu dem Aufruf gehörende Frame vom Call Stack genommen wird.
Es sei hierzu angemerkt, dass es sich bei diesem Beispiel lediglich um eine schematische Darstellung handelt, um die Funktionsweise von dynamischem Scope zu veranschaulichen. Die Ausführungen sind nicht so zu interpretieren, dass in der Praxis tatsächlich der Call Stack nach Namensbindungen durchsucht wird. Für die Buchführung darüber, welche Bindung in der Kette der Aufrufe am nächsten liegt, wird man wahrscheinlich eine separate Datenstruktur verwenden wollen.
Jedenfalls können wir festhalten, dass man bei dynamischem Scope in aller Regel ahead of time nicht bestimmen kann, welchen Wert eine Variable hat, die außerhalb einer bestimmten Funktion definiert ist, oder ob besagte Variable überhaupt sichtbar ist. Eine statische Analyse des Codes stößt hier schnell an Grenzen, da die Sichtbarkeit von Variablen und Funktionen immer vom gegenwärtigen Zustand des Programms abhängt.
Da Code den man nur schwer nachvollziehen kann naturgemäß Fehler begünstigt, wird dynamischer Scope selten verwendet und in den meisten Sprachen nicht einmal als optionales Feature angeboten. Stattdessen verwenden wir – vielleicht oft ohne darüber nachzudenken – ganz selbstverständlich statischen, beziehungsweise lexikalischen Scope. Also Regeln für die Sichtbarkeit von Bezeichnern, bei denen man durch bloßen Augenschein sagen kann, ob eine Funktion eine Variable „kennt“.
Viele Grüße,
Orlok
--
„Dance like it hurts.
Make love like you need money.
Work when people are watching.“ — Dogbert