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-continuation
qui est souvent abrégée en callcc
. Malheureusement, callcc
ne 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 callcc
fonction prend une fonction f
et l'applique au current-continuation
(abrégé en cc
). La current-continuation
est 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-continuation
de la seconde callcc
est:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
De même, le current-continuation
premier callcc
est:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Puisque le current-continuation
du premier en callcc
contient un autre, callcc
il 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, callcc
convertit 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 callcc
en 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 callcc
peut ê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.