Acredito que as continuações são um caso especial de retornos de chamada. Uma função pode retornar qualquer número de funções, qualquer número de vezes. Por exemplo:
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);
}
No entanto, se uma função chama de volta outra função como a última coisa que faz, a segunda função é chamada de continuação da primeira. Por exemplo:
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);
}
}
}
Se uma função chama outra função como a última coisa que faz, então é chamada de chamada final. Alguns idiomas como o Scheme realizam otimizações de chamada de cauda. Isso significa que a chamada final não incorre na sobrecarga completa de uma chamada de função. Em vez disso, é implementado como um simples goto (com o quadro da pilha da função de chamada substituído pelo quadro da pilha da chamada final).
Bônus : Prosseguindo para o estilo de passagem continuada. Considere o seguinte programa:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Agora, se todas as operações (incluindo adição, multiplicação, etc.) fossem escritas na forma de funções, terí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;
}
Além disso, se não nos fosse permitido retornar valores, teríamos que usar continuações da seguinte maneira:
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);
}
Esse estilo de programação em que você não tem permissão para retornar valores (e, portanto, você deve recorrer a repetições) é chamado de estilo de passagem de continuação.
No entanto, existem dois problemas com o estilo de passagem de continuação:
- Passar por continuações aumenta o tamanho da pilha de chamadas. A menos que você esteja usando uma linguagem como o Scheme, que elimina chamadas finais, você corre o risco de ficar sem espaço na pilha.
- É difícil escrever funções aninhadas.
O primeiro problema pode ser facilmente resolvido no JavaScript chamando continuações de forma assíncrona. Chamando a continuação de forma assíncrona, a função retorna antes que a continuação seja chamada. Portanto, o tamanho da pilha de chamadas não 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);
}
O segundo problema geralmente é resolvido usando uma função chamada call-with-current-continuation
que é frequentemente abreviada como callcc
. Infelizmente, callcc
não pode ser totalmente implementado em JavaScript, mas poderíamos escrever uma função de substituição para a maioria de seus 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;
}
A callcc
função pega uma função f
e aplica-a ao current-continuation
(abreviado como cc
). A current-continuation
é uma função de continuação que envolve o restante do corpo da função após a chamada para callcc
.
Considere o corpo da função pythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
O current-continuation
segundo callcc
é:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
Da mesma forma, o current-continuation
primeiro callcc
é:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Como o current-continuation
primeiro callcc
contém outro, callcc
ele deve ser convertido para o estilo de passagem de continuação:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
Então, basicamente, callcc
logicamente converte todo o corpo da função de volta ao que começamos (e dá o nome a essas funções anônimas cc
). A função de pitágoras usando esta implementação de callcc passa a ser:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Novamente, você não pode implementar callcc
em JavaScript, mas pode implementá-lo no estilo de passagem de continuação em JavaScript da seguinte maneira:
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);
}
A função callcc
pode ser usada para implementar estruturas complexas de fluxo de controle, como blocos try-catch, coroutines, geradores, fibras , etc.