Uważam, że kontynuacje to szczególny przypadek callbacków. Funkcja może wywołać dowolną liczbę funkcji, dowolną liczbę razy. Na przykład:
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);
}
Jeśli jednak funkcja wywołuje inną funkcję jako ostatnia czynność, wówczas druga funkcja jest nazywana kontynuacją pierwszej. Na przykład:
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;
cont(0);
function cont(index) {
if (index < length) {
callback(array[index], array, index);
cont(++index);
}
}
}
Jeśli funkcja wywołuje inną funkcję jako ostatnia rzecz, którą robi, nazywa się to wywołaniem ogonowym. Niektóre języki, takie jak Scheme, wykonują optymalizacje połączeń końcowych. Oznacza to, że wywołanie ogonowe nie pociąga za sobą pełnego narzutu wywołania funkcji. Zamiast tego jest zaimplementowany jako proste goto (z ramką stosu funkcji wywołującej zastąpioną ramką stosu wywołania końcowego).
Bonus : przejście do kontynuacji stylu podań. Rozważ następujący program:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Otóż, gdyby każda operacja (w tym dodawanie, mnożenie itp.) Była zapisana w postaci funkcji, otrzymalibyśmy:
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;
}
Ponadto, gdybyśmy nie mogli zwrócić żadnych wartości, musielibyśmy użyć kontynuacji w następujący sposób:
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);
}
Ten styl programowania, w którym nie możesz zwracać wartości (i dlatego musisz uciekać się do przekazywania kontynuacji) jest nazywany stylem przekazywania kontynuacji.
Istnieją jednak dwa problemy ze stylem przekazywania kontynuacji:
- Przekazywanie kontynuacji zwiększa rozmiar stosu wywołań. Jeśli nie używasz języka takiego jak Scheme, który eliminuje wywołania końcowe, ryzykujesz brak miejsca na stosie.
- Pisanie funkcji zagnieżdżonych jest uciążliwe.
Pierwszy problem można łatwo rozwiązać w JavaScript, wywołując kontynuacje asynchronicznie. Wywołując kontynuację asynchronicznie, funkcja zwraca przed wywołaniem kontynuacji. Stąd rozmiar stosu wywołań nie zwiększa się:
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);
}
Drugi problem jest zwykle rozwiązywany za pomocą funkcji o nazwie, call-with-current-continuation
która jest często określana skrótem callcc
. Niestety callcc
nie można go w pełni zaimplementować w JavaScript, ale moglibyśmy napisać funkcję zastępującą dla większości jego przypadków użycia:
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;
}
callcc
Funkcja przyjmuje funkcję f
i zastosowanie go do current-continuation
(w skrócie cc
). Jest current-continuation
to funkcja kontynuacji, która po wywołaniu zamyka resztę treści funkcji callcc
.
Rozważmy treść funkcji pythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
current-continuation
Drugiego callcc
jest:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
Podobnie current-continuation
pierwszy callcc
to:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Ponieważ current-continuation
pierwszy callcc
zawiera inny callcc
, należy go przekonwertować na styl przekazywania kontynuacji:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
Zasadniczo więc callcc
logicznie konwertuje całą treść funkcji z powrotem do tego, od czego zaczęliśmy (i nadaje tym anonimowym funkcjom nazwę cc
). Funkcja Pitagorasa używająca tej implementacji callcc staje się wtedy:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Ponownie nie możesz zaimplementować callcc
w JavaScript, ale możesz zaimplementować styl przekazywania kontynuacji w JavaScript w następujący sposób:
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);
}
Ta funkcja callcc
może być używana do implementacji złożonych struktur przepływu sterowania, takich jak bloki try-catch, coroutines, generatory, włókna itp.