Creo que las continuaciones son un caso especial de devoluciones de llamada. Una función puede devolver cualquier cantidad de funciones, cualquier cantidad de veces. Por ejemplo:
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);
}
Sin embargo, si una función vuelve a llamar a otra función como lo último que hace, la segunda función se llama continuación de la primera. Por ejemplo:
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);
}
}
}
Si una función llama a otra función como lo último que hace, entonces se llama una llamada de cola. Algunos lenguajes como Scheme realizan optimizaciones de llamada de cola. Esto significa que la llamada de cola no incurre en la sobrecarga total de una llamada de función. En cambio, se implementa como un simple goto (con el marco de la pila de la función de llamada reemplazado por el marco de la pila de la llamada de cola).
Bonificación : proceder al estilo de pase de continuación. Considere el siguiente programa:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Ahora, si cada operación (incluida la suma, multiplicación, etc.) se escribiera en forma de funciones, tendríamos:
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;
}
Además, si no se nos permitiera devolver ningún valor, tendríamos que usar las siguientes continuaciones:
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);
}
Este estilo de programación en el que no se le permite devolver valores (y, por lo tanto, debe recurrir a pasar las continuaciones) se llama estilo de paso de continuación.
Sin embargo, hay dos problemas con el estilo de pase de continuación:
- Pasar las continuaciones aumenta el tamaño de la pila de llamadas. A menos que esté utilizando un lenguaje como Scheme que elimine las llamadas de cola, correrá el riesgo de quedarse sin espacio de pila.
- Es un dolor escribir funciones anidadas.
El primer problema se puede resolver fácilmente en JavaScript llamando a las continuaciones de forma asincrónica. Al llamar a la continuación de forma asincrónica, la función vuelve antes de que se llame a la continuación. Por lo tanto, el tamaño de la pila de llamadas no aumenta:
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);
}
El segundo problema generalmente se resuelve usando una función llamada call-with-current-continuation
que a menudo se abrevia como callcc
. Desafortunadamente callcc
, no se puede implementar completamente en JavaScript, pero podríamos escribir una función de reemplazo para la mayoría de sus casos de uso:
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;
}
La callcc
función toma una función f
y la aplica a current-continuation
(abreviada como cc
). La current-continuation
es una función de continuación que envuelve el resto del cuerpo de la función después de la llamada a callcc
.
Considere el cuerpo de la función pythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
El current-continuation
de la segunda callcc
es:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
Del mismo modo, el current-continuation
primero callcc
es:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Como el current-continuation
primero callcc
contiene otro callcc
, debe convertirse al estilo de paso de continuación:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
Así que, esencialmente, callcc
convierte lógicamente todo el cuerpo de la función a lo que comenzamos (y le da el nombre a esas funciones anónimas cc
). La función de Pitágoras que usa esta implementación de callcc se convierte en:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Nuevamente, no puede implementar callcc
en JavaScript, pero puede implementar el estilo de paso de continuación en JavaScript de la siguiente manera:
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);
}
La función callcc
se puede utilizar para implementar estructuras de flujo de control complejas, como bloques try-catch, corutinas, generadores, fibras , etc.