Ich glaube, dass Fortsetzungen ein Sonderfall von Rückrufen sind. Eine Funktion kann eine beliebige Anzahl von Funktionen und eine beliebige Anzahl von Malen zurückrufen. Beispielsweise:
var array = [1, 2, 3];
forEach(array, function (element, array, index) {
array[index] = 2 * element;
});
console.log(array);
function forEach(array, callback) {
var length = array.length;
for (var i = 0; i < length; i++)
callback(array[i], array, i);
}
Wenn eine Funktion jedoch als letztes eine andere Funktion zurückruft, wird die zweite Funktion als Fortsetzung der ersten bezeichnet. Beispielsweise:
var array = [1, 2, 3];
forEach(array, function (element, array, index) {
array[index] = 2 * element;
});
console.log(array);
function forEach(array, callback) {
var length = array.length;
// This is the last thing forEach does
// cont is a continuation of forEach
cont(0);
function cont(index) {
if (index < length) {
callback(array[index], array, index);
// This is the last thing cont does
// cont is a continuation of itself
cont(++index);
}
}
}
Wenn eine Funktion als letztes eine andere Funktion aufruft, wird sie als Tail-Aufruf bezeichnet. Einige Sprachen wie Scheme führen Tail-Call-Optimierungen durch. Dies bedeutet, dass der Endaufruf nicht den vollen Aufwand eines Funktionsaufrufs verursacht. Stattdessen wird es als einfaches goto implementiert (wobei der Stapelrahmen der aufrufenden Funktion durch den Stapelrahmen des Endaufrufs ersetzt wird).
Bonus : Weiter zum Weiterbestehen. Betrachten Sie das folgende Programm:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Wenn nun jede Operation (einschließlich Addition, Multiplikation usw.) in Form von Funktionen geschrieben wäre, hätten wir:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return add(square(x), square(y));
}
function square(x) {
return multiply(x, x);
}
function multiply(x, y) {
return x * y;
}
function add(x, y) {
return x + y;
}
Wenn wir keine Werte zurückgeben könnten, müssten wir außerdem die folgenden Fortsetzungen verwenden:
pythagoras(3, 4, console.log);
function pythagoras(x, y, cont) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply(x, x, cont);
}
function multiply(x, y, cont) {
cont(x * y);
}
function add(x, y, cont) {
cont(x + y);
}
Dieser Programmierstil, bei dem Sie keine Werte zurückgeben dürfen (und daher auf die Weitergabe von Fortsetzungen zurückgreifen müssen), wird als Weitergabestil für Fortsetzungen bezeichnet.
Es gibt jedoch zwei Probleme mit dem Weitergabestil:
- Das Weitergeben von Fortsetzungen erhöht die Größe des Aufrufstapels. Wenn Sie keine Sprache wie Scheme verwenden, die Tail Calls eliminiert, besteht die Gefahr, dass Ihnen der Stapelspeicher ausgeht.
- Es ist mühsam, verschachtelte Funktionen zu schreiben.
Das erste Problem kann in JavaScript leicht gelöst werden, indem Fortsetzungen asynchron aufgerufen werden. Durch asynchrones Aufrufen der Fortsetzung kehrt die Funktion zurück, bevor die Fortsetzung aufgerufen wird. Daher nimmt die Größe des Aufrufstapels nicht zu:
Function.prototype.async = async;
pythagoras.async(3, 4, console.log);
function pythagoras(x, y, cont) {
square.async(x, function (x_squared) {
square.async(y, function (y_squared) {
add.async(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply.async(x, x, cont);
}
function multiply(x, y, cont) {
cont.async(x * y);
}
function add(x, y, cont) {
cont.async(x + y);
}
function async() {
setTimeout.bind(null, this, 0).apply(null, arguments);
}
Das zweite Problem wird normalerweise mit einer Funktion gelöst, call-with-current-continuation
die oft als abgekürzt wird callcc
. Leider callcc
kann nicht vollständig in JavaScript implementiert werden, aber wir könnten für die meisten Anwendungsfälle eine Ersatzfunktion schreiben:
pythagoras(3, 4, console.log);
function pythagoras(x, y, cont) {
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
function square(x, cont) {
multiply(x, x, cont);
}
function multiply(x, y, cont) {
cont(x * y);
}
function add(x, y, cont) {
cont(x + y);
}
function callcc(f) {
var cc = function (x) {
cc = x;
};
f(cc);
return cc;
}
Die callcc
Funktion nimmt eine Funktion auf f
und wendet sie auf die current-continuation
(abgekürzt als cc
) an. Dies current-continuation
ist eine Fortsetzungsfunktion, die den Rest des Funktionskörpers nach dem Aufruf von einschließt callcc
.
Betrachten Sie den Hauptteil der Funktion pythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
Das current-continuation
zweite callcc
ist:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
In ähnlicher Weise ist current-continuation
der erste callcc
:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Da der current-continuation
erste callcc
einen anderen enthält callcc
, muss er in den Fortsetzungsstil konvertiert werden:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
callcc
Konvertiert also im Wesentlichen logisch den gesamten Funktionskörper zurück zu dem, von dem wir ausgegangen sind (und gibt diesen anonymen Funktionen den Namen cc
). Die Pythonagoras-Funktion, die diese Implementierung von callcc verwendet, wird dann:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Auch hier können Sie nicht callcc
in JavaScript implementieren , aber Sie können den Continuation-Passing-Stil in JavaScript wie folgt implementieren:
Function.prototype.async = async;
pythagoras.async(3, 4, console.log);
function pythagoras(x, y, cont) {
callcc.async(square.bind(null, x), function cc(x_squared) {
callcc.async(square.bind(null, y), function cc(y_squared) {
add.async(x_squared, y_squared, cont);
});
});
}
function square(x, cont) {
multiply.async(x, x, cont);
}
function multiply(x, y, cont) {
cont.async(x * y);
}
function add(x, y, cont) {
cont.async(x + y);
}
function async() {
setTimeout.bind(null, this, 0).apply(null, arguments);
}
function callcc(f, cc) {
f.async(cc);
}
Die Funktion callcc
kann verwendet werden, um komplexe Kontrollflussstrukturen wie Try-Catch-Blöcke, Coroutinen, Generatoren, Fasern usw. zu implementieren .