Credo che le continuazioni siano un caso speciale di richiamate. Una funzione può richiamare un numero qualsiasi di funzioni, un numero qualsiasi di volte. Per esempio:
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);
}
Tuttavia, se una funzione richiama un'altra funzione come ultima cosa, la seconda funzione viene chiamata continuazione della prima. Per esempio:
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 una funzione chiama un'altra funzione come ultima cosa che fa, allora si chiama coda. Alcune lingue come Scheme eseguono l'ottimizzazione delle chiamate di coda. Ciò significa che la chiamata di coda non comporta l'intero overhead di una chiamata di funzione. Invece è implementato come un semplice goto (con il frame dello stack della funzione di chiamata sostituito dal frame dello stack della chiamata di coda).
Bonus : procedere allo stile di passaggio di continuazione. Considera il seguente programma:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Ora se ogni operazione (inclusa aggiunta, moltiplicazione, ecc.) Fosse scritta sotto forma di funzioni, avremmo:
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;
}
Inoltre, se non ci fosse permesso di restituire alcun valore, dovremmo usare le continuazioni come segue:
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);
}
Questo stile di programmazione in cui non è consentito restituire valori (e quindi è necessario ricorrere a continuazioni di passaggio) viene chiamato stile di passaggio di continuazione.
Vi sono tuttavia due problemi con lo stile del passaggio di continuazione:
- Il passaggio di continuazioni aumenta la dimensione dello stack di chiamate. A meno che tu non stia usando un linguaggio come Scheme che elimina le chiamate di coda, rischierai di rimanere senza spazio nello stack.
- Scrivere funzioni nidificate è una seccatura.
Il primo problema può essere facilmente risolto in JavaScript chiamando le continuazioni in modo asincrono. Chiamando la continuazione in modo asincrono, la funzione ritorna prima che venga chiamata la continuazione. Quindi la dimensione dello stack di chiamate non 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);
}
Il secondo problema è di solito risolto usando una funzione chiamata call-with-current-continuation
che è spesso abbreviata come callcc
. Purtroppo callcc
non può essere completamente implementato in JavaScript, ma potremmo scrivere una funzione di sostituzione per la maggior parte dei suoi casi d'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
funzione accetta una funzione f
e la applica a current-continuation
(abbreviato come cc
). La current-continuation
è una funzione continuazione che avvolge il resto del corpo funzione dopo la chiamata a callcc
.
Considera il corpo della funzione pythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
Il current-continuation
secondo callcc
è:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
Allo stesso modo il current-continuation
primo callcc
è:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Poiché il current-continuation
primo callcc
contiene un altro callcc
, deve essere convertito in stile di passaggio di continuazione:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
Quindi essenzialmente callcc
ricalca logicamente l'intero corpo della funzione in ciò da cui siamo partiti (e dà a quelle funzioni anonime il nome cc
). La funzione pitagora che utilizza questa implementazione di callcc diventa quindi:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Ancora una volta non è possibile implementare callcc
in JavaScript, ma è possibile implementare lo stile di passaggio di continuazione in JavaScript come segue:
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 funzione callcc
può essere utilizzata per implementare complesse strutture di flusso di controllo come blocchi try-catch, coroutine, generatori, fibre , ecc.