Konsole & …: Funktionsabschlüsse

the next big thing  –  3 Kommentare

"Konsole & Kontext" ist eine gemeinsame Serie von Golo Roden und Michael Wiedeking, in der sich die beiden regelmäßig mit Konzepten der (funktionalen) Programmierung beschäftigen. Während "Konsole & …" die praktische Anwendung dieser Konzepte in JavaScript und Node.js zeigt, erläutert "... & Kontext" deren jeweiligen eher theoretischen Hintergrund.

Die vergangenen Folgen haben Funktionsausdrücke als Kernelement funktionaler Programmierung vorgestellt und anschließend gezeigt, wie man aus ihnen Funktionen höherer Ordnung erzeugt. Den Funktionen höherer Ordnung war jedoch stets gemein, dass die äußeren und inneren Funktionen keinerlei Bezug zueinander hatten und daher unabhängig voneinander waren.

Als Beispiel hierfür dient die Funktion getAdder, die als Funktion höherer Ordnung fungiert und eine weitere Funktion zur Addition zweier Zahlen zurückgibt:

var getAdder = function () {
return function (first, second) {
return first + second;
};
};
var add = getAdder();

Außer der Tatsache, dass die Funktion getAdder eine Funktion an den Aufrufer zurückgibt, gibt es keine Verbindung zwischen den beiden Funktionen. Die zurückgegebene Funktion würde auf die gleiche Weise funktionieren, wenn man sie außerhalb von getAdder definieren und ihr einen Namen zuweisen würde:

var add = function (first, second) {
return first + second;
};

Anders verhält es sich jedoch, wenn man der äußeren Funktionen Daten als Parameter übergibt und die innere Funktion auf diese zugreift:

var getAdder = function (first) {
return function (second) {
return first + second;
};
};
var add = getAdder(23);

Die Funktion add verfügt nun nur noch über einen einzigen Parameter. Dementsprechend genügt bei ihrem Aufruf auch die Angabe dieses einen Parameters:

var sum = add(42);
console.log(sum); // => 65

Wie das Beispiel zeigt, führt die Funktion add die Addition trotzdem korrekt aus und "erinnert" sich quasi an den Wert des Parameters first der äußeren Funktion. Das ist deshalb bemerkenswert, weil die äußere Funktion zum Zeitpunkt des Aufrufs von add längst beendet wurde und daher auch ihre Parameter nicht mehr im Stack enthalten sind.

Den ursprünglichen Kontext merken

Daraus kann man schließen, dass sich die innere Funktion den zum Zeitpunkt ihrer Erstellung gültigen Kontext merkt und auf diesen zu einem späteren Zeitpunkt wieder zugreifen kann – und zwar unabhängig davon, ob die ursprüngliche äußere Funktion noch ausgeführt wird oder nicht.

Eine derartige innere Funktion bezeichnet man als "Funktionsabschluss" oder, im Englischen, als "Closure". Dabei gilt es zu beachten, dass sich die innere Funktion die Variable selbst und nicht deren Wert merkt. Daher geben die Anweisungen:

var functions = [],
i;

for (i = 0; i < 10; i++) {
functions.push(function () {
console.log(i);
});
}

functions.forEach(function (fn) {
fn();
});

nicht, wie man zunächst eventuell vermuten könnte, die Zahlen von 0 bis 9 aus, sondern zehnmal den Wert 10. Dies ist zwar nicht intuitiv, aber durchaus korrekt, da die Variable i nach dem Ende der for-Schleife ebendiesen Wert enthält.

Will man tatsächlich den Wert der Variablen i im jeweiligen Schleifendurchlauf erhalten, muss man den Inhalt der Schleife in einen augenblicklich ausgewerteten Funktionsausdruck einschließen. Da JavaScript diesen in jedem Durchlauf der Schleife neu erzeugt, referenzieren die in der Schleife erzeugten Funktionen allesamt unterschiedliche Variablen – und damit auch verschiedene Werte:

var functions = [],
i;

for (i = 0; i < 10; i++) {
(function (i) {
functions.push(function () {
console.log(i);
});
})(i);
}

functions.forEach(function (fn) {
fn();
});

Private Variablen nachbilden

Der besondere Nutzen von Funktionsabschlüssen besteht darin, dass man sie verwenden kann, um private Variablen nachbilden. Der entscheidende Punkt hierbei ist, dass die innere Funktion zwar Zugriff auf den ursprünglichen Kontext erhält, dieser von außen aber nicht zugänglich ist.

Das Beispiel der getAdder-Funktion verdeutlicht das auf anschauliche Weise: Nachdem sie die innere Funktion zurückgegeben hat, kann man ausschließlich deren Parameter second von außen beeinflussen. Der Wert des Parameters first der äußeren Funktion hingegen ist eingeschlossen und vor der Außenwelt verborgen.

Das gleiche Prinzip greift, wenn man mit JavaScript objektorientiert entwickelt. Das folgende Beispiel zeigt eine einfache Implementierung einer Stack-Klasse:

function Stack () {
this.data = [];
}

Stack.prototype.push = function (value) {
this.data.unshift(value);
};

Stack.prototype.pop = function () {
return this.data.shift();
};

Stack.prototype.top = function () {
return this.data[0];
};

Stack.prototype.isEmpty = function () {
return this.data.length === 0;
};

Ein einfacher Test zeigt zudem, dass diese Klasse prinzipiell wie gewünscht funktioniert:

var s = new Stack();
s.push(23);
s.push(42);

console.log(s.isEmpty()); // => false
console.log(s.top()); // => 42
console.log(s.pop()); // => 42
console.log(s.pop()); // => 23
console.log(s.isEmpty()); // => true

Nachteilig an der gewählten Implementierung ist allerdings, dass man auf die data-Eigenschaft von außen zugreifen kann. Das verletzt das besonders in der objektorientierten Programmierung verbreitete Prinzip der Datenkapselung.

Die Lösung für dieses Problem besteht darin, an Stelle einer Konstruktorfunktion und des this-Verweises eine Reihe von Funktionsabschlüssen zu verwenden, um einen Stack zu erzeugen:

var stack = {
create: function () {
var data = [];

return {
push: function (value) {
data.unshift(value);
},

pop: function () {
return data.shift();
},

top: function () {
return data[0];
},

isEmpty: function () {
return data.length === 0;
}
};
}
};

Da data nun eine lokale Variable der create-Funktion ist, kann man per Definition nicht von außen auf sie zugreifen. Den als Funktionsabschlüssen implementierten Funktionen des Stacks ist das allerdings durchaus möglich. Letztlich stellt data daher nun eine private Variable dar.

Die Verwendung des Stacks hat sich, abgesehen vom initialen Erzeugen einer neuen Instanz, nicht verändert:

var s = stack.create();
s.push(23);
s.push(42);

console.log(s.isEmpty()); // => false
console.log(s.top()); // => 42
console.log(s.pop()); // => 42
console.log(s.pop()); // => 23
console.log(s.isEmpty()); // => true

Erwähnt sei allerdings, dass diese Art zu entwickeln durchaus auch Einbußen im Hinblick auf die Leistung und den Speicherbedarf aufweist. Deren Relevanz in der Praxis muss man aber von Fall zu Fall individuell bewerten, weshalb an dieser Stelle nicht näher darauf eingegangen wird.

Für zahlreiche Anwendungen dürften die Vorteile die erwähnten Nachteile jedoch bei weitem aufwiegen, sodass Funktionsabschlüsse ein äußerst hilfreiches Werkzeug für die funktionale Programmierung sind.

tl;dr: Funktionsabschlüsse sind Funktionen, die sich den ursprünglichen zum Zeitpunkt ihrer Erzeugung gültigen Kontext merken und auf diesen später wieder zugreifen können. Sie dienen unter anderem dazu, private Variablen zu implementieren.