Je crois que les continuations sont un cas particulier de rappels. Une fonction peut rappeler n'importe quel nombre de fonctions, n'importe quel nombre de fois. Par exemple:
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);
}
Cependant, si une fonction rappelle une autre fonction en dernier lieu, alors la deuxième fonction est appelée une continuation de la première. Par exemple:
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 une fonction appelle une autre fonction en dernier lieu, elle s'appelle un appel de queue. Certains langages comme Scheme effectuent des optimisations des appels de fin. Cela signifie que l'appel de fin n'entraîne pas la surcharge totale d'un appel de fonction. Au lieu de cela, il est implémenté comme un simple goto (avec le cadre de pile de la fonction appelante remplacé par le cadre de pile de l'appel de queue).
Bonus : passez au style de passe de continuation. Considérez le programme suivant:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Maintenant, si chaque opération (y compris l'addition, la multiplication, etc.) était écrite sous forme de fonctions, alors nous aurions:
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;
}
De plus, si nous n'étions pas autorisés à renvoyer des valeurs, nous devrons utiliser les continuations comme suit:
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);
}
Ce style de programmation dans lequel vous n'êtes pas autorisé à renvoyer des valeurs (et par conséquent, vous devez recourir à la transmission de continuations) est appelé style de passage de continuation.
Il y a cependant deux problèmes avec le style de passage de continuation:
- La transmission de continuations augmente la taille de la pile d'appels. À moins d'utiliser un langage comme Scheme qui élimine les appels de queue, vous risquez de manquer d'espace dans la pile.
- C'est pénible d'écrire des fonctions imbriquées.
Le premier problème peut être facilement résolu en JavaScript en appelant les continuations de manière asynchrone. En appelant la continuation de manière asynchrone, la fonction retourne avant que la continuation ne soit appelée. Par conséquent, la taille de la pile d'appels n'augmente pas:
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);
}
Le deuxième problème est généralement résolu en utilisant une fonction appelée call-with-current-continuationqui est souvent abrégée en callcc. Malheureusement, callccne peut pas être entièrement implémenté en JavaScript, mais nous pourrions écrire une fonction de remplacement pour la plupart de ses cas d'utilisation:
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 callccfonction prend une fonction fet l'applique au current-continuation(abrégé en cc). La current-continuationest une fonction de continuation qui enveloppe le reste du corps de la fonction après l'appel à callcc.
Considérez le corps de la fonction pythagoras:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
Le current-continuationde la seconde callccest:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
De même, le current-continuationpremier callccest:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Puisque le current-continuationdu premier en callcccontient un autre, callccil doit être converti en style de passage de continuation:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
Donc, essentiellement, callccconvertit logiquement tout le corps de la fonction en ce que nous avons commencé (et donne le nom à ces fonctions anonymes cc). La fonction pythagoras utilisant cette implémentation de callcc devient alors:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Encore une fois, vous ne pouvez pas implémenter callccen JavaScript, mais vous pouvez l'implémenter le style de passage de continuation en JavaScript comme suit:
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 fonction callccpeut être utilisée pour implémenter des structures de flux de contrôle complexes telles que des blocs try-catch, des coroutines, des générateurs, des fibres , etc.