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-continuationdie oft als abgekürzt wird callcc. Leider callcckann 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 callccFunktion nimmt eine Funktion auf fund wendet sie auf die current-continuation(abgekürzt als cc) an. Dies current-continuationist 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-continuationzweite callccist:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
In ähnlicher Weise ist current-continuationder erste callcc:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Da der current-continuationerste callcceinen 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);
});
}
callccKonvertiert 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 callccin 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 callcckann verwendet werden, um komplexe Kontrollflussstrukturen wie Try-Catch-Blöcke, Coroutinen, Generatoren, Fasern usw. zu implementieren .