Я вважаю, що продовження - це особливий випадок зворотних дзвінків. Функція може відкликати будь-яку кількість функцій, будь-яку кількість разів. Наприклад:
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);
}
Однак якщо функція викликає ще одну функцію як останнє, що вона робить, то друга функція називається продовженням першої. Наприклад:
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);
}
}
}
Якщо функція викликає іншу функцію як останнє, що вона робить, то вона називається хвостовим викликом. Деякі мови, як-от Схема, виконують оптимізацію хвостових викликів. Це означає, що хвостовий виклик не здійснює повних накладних витрат функціонального виклику. Натомість він реалізований як простий goto (кадр стека викличної функції замінюється рамкою стека хвостового виклику).
Бонус : Перехід до стилю продовження проходження. Розглянемо наступну програму:
console.log(pythagoras(3, 4));
function pythagoras(x, y) {
return x * x + y * y;
}
Тепер, якби кожна операція (включаючи додавання, множення тощо) була записана у вигляді функцій, тоді ми мали б:
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;
}
Крім того, якби нам не дозволяли повертати будь-які значення, тоді нам доведеться використовувати наступні дії:
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);
}
Цей стиль програмування, в якому вам заборонено повертати значення (а значить, ви повинні вдатися до передачі продовження навколо), називається стилем передачі продовження.
Однак існують дві проблеми із стилем продовження проходження:
- Проходження довкола продовження збільшує розмір стека викликів. Якщо ви не використовуєте мову на зразок Scheme, яка усуває хвостові дзвінки, ви ризикуєте втратити місце у стеку.
- Боляче писати вкладені функції.
Першу проблему можна легко вирішити в JavaScript, викликаючи продовження асинхронно. Викликаючи продовження асинхронно, функція повертається до виклику продовження. Отже, розмір стека виклику не збільшується:
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);
}
Друга проблема зазвичай вирішується за допомогою функції, call-with-current-continuation
яка називається, яка часто скорочується як callcc
. На жаль, callcc
не можна повністю реалізувати в JavaScript, але ми можемо написати функцію заміни для більшості випадків використання:
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;
}
callcc
Функція приймає функцію f
і застосовує його до current-continuation
(скорочено cc
). Функція current-continuation
продовження, яка завершує залишок функції функції після виклику до callcc
.
Розглянемо тіло функції pythagoras
:
var x_squared = callcc(square.bind(null, x));
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
current-continuation
Другий callcc
є:
function cc(y_squared) {
add(x_squared, y_squared, cont);
}
Аналогічно current-continuation
першому callcc
є:
function cc(x_squared) {
var y_squared = callcc(square.bind(null, y));
add(x_squared, y_squared, cont);
}
Оскільки current-continuation
перший callcc
містить інший, callcc
його потрібно перетворити на стиль продовження проходження:
function cc(x_squared) {
square(y, function cc(y_squared) {
add(x_squared, y_squared, cont);
});
}
Таким чином, по суті callcc
логічно перетворюється все тіло функції назад до того, з чого ми почали (і дає цим анонімним функціям назву cc
). Функція піфагора за допомогою цієї реалізації callcc стає тоді:
function pythagoras(x, y, cont) {
callcc(function(cc) {
square(x, function (x_squared) {
square(y, function (y_squared) {
add(x_squared, y_squared, cont);
});
});
});
}
Знову ви не можете реалізувати callcc
в JavaScript, але ви можете реалізувати його стиль продовження проходження в JavaScript наступним чином:
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);
}
Ця функція callcc
може бути використана для реалізації складних структур управління потоком, таких як блоки спробу-захоплення, розробки, генератори, волокна тощо.