Хвостовая рекурсия в NodeJS

Итак, недавно я столкнулся со случаем, когда мне нужно было написать код, в котором обратный вызов вызывает сам себя и т. д., и я задавался вопросом о поддержке NodeJS и хвостового вызова, поэтому я нашел этот ответ https://stackoverflow.com/a/30369729 говорит, что да, это поддерживается.

Итак, я попробовал это с помощью этого простого кода:

"use strict";
function fac(n){
    if(n==1){
        console.trace();
        return 1;
    }
    return n*fac(n-1);
}

fac(5);

Используя Node 6.9.2 в Linux x64 и запустив его как node tailcall.js --harmony --harmony_tailcalls --use-strict, мы получили следующий результат:

Trace
    at fac (/home/tailcall.js:4:11)
    at fac (/home/tailcall.js:7:11)
    at fac (/home/tailcall.js:7:11)
    at fac (/home/tailcall.js:7:11)
    at fac (/home/tailcall.js:7:11)
    at Object.<anonymous> (/home/tailcall.js:10:1)
    at Module._compile (module.js:570:32)
    at Object.Module._extensions..js (module.js:579:10)
    at Module.load (module.js:487:32)
    at tryModuleLoad (module.js:446:12)

Что ясно показывает, что стек вызовов заполняется вызовами, а хвостовая рекурсия не поддерживается, хотя я использую последнюю версию NodeJS.

Поддерживает ли вообще NodeJS/JavaScript хвостовую рекурсию? Или мне действительно нужно использовать генераторы и выходы, но проблема здесь в том, что мои обратные вызовы будут сильно асинхронными, и я все равно не буду работать с возвращаемым значением, мне просто нужно убедиться, что стек вызовов не будет бесполезно заполнен вызовы, в то время как функция ссылается на себя в ответ.


person Community    schedule 15.01.2017    source источник
comment
Вы можете переписать, используя цикл while вместо рекурсии.   -  person jfriend00    schedule 16.01.2017
comment
Как я уже говорил в вопросе, меня не волнует возвращаемый результат, поскольку обратные вызовы сильно зависят от асинхронного ввода-вывода, поэтому использование while невозможно.   -  person    schedule 16.01.2017
comment
Я предлагаю вам показать фактический пример, который вас беспокоит, потому что вызов самой функции из асинхронного обратного вызова на самом деле не приводит к накоплению стека, поскольку исходная функция уже вернулась до того, как будет вызван асинхронный обратный вызов. Таким образом, мы, вероятно, могли бы лучше помочь вам с вашей реальной проблемой, если бы вы показали свою реальную проблему.   -  person jfriend00    schedule 16.01.2017


Ответы (3)


Во-первых, если ваш фактический случай, о котором вы беспокоитесь, - это когда функция вызывает себя из асинхронного обратного вызова, то у вас, вероятно, в любом случае нет накопления стека.

Это связано с тем, что исходная функция уже вернулась, и весь стек раскручивается до вызова асинхронного обратного вызова, поэтому, хотя визуально это выглядит как рекурсия, стек не накапливается.

Вот простой пример кода выполнения нескольких последовательных сетевых запросов, когда функция вызывает сама себя из асинхронного обратного вызова и нет накопления стека.

function getPages(baseURL, startParam, endParam, progressCallback) {

    function run(param) {
         request(baseURL + param, function(err, response, body) {
             if (err) return progressCallback(err);
             ++param;
             if (param < endParam) {
                 progressCallback(null, body);
                 // run another iteration
                 // there is no stack buildup with this call
                 run(param);
             } else {
                 // indicate completion of all calls
                 progressCallback(null, null);
             }
         });
    }
    run(startParam);
}

Предыдущий вызов run() уже вернулся, и стек полностью раскрутился до вызова асинхронного обратного вызова, поэтому, хотя это визуально выглядит как рекурсия, накопления стека нет.


В конкретном коде, который вы показываете, вы можете полностью избежать рекурсии, переписав его с помощью цикла while, который будет эффективно работать в любой версии Javascript:

function fac(n){
    var total = 1;
    while (n > 1) {
        total *= n--;
    }
    return total;
}

// run demo in snippet
for (var i = 1; i <= 10; i++) {
    console.log(i, fac(i))
}

person jfriend00    schedule 15.01.2017
comment
Иногда это невозможно, когда функция обратного вызова зависит от асинхронного ввода-вывода. - person ; 16.01.2017
comment
@jakubinf - Ну, это зависит от того, о каком именно коде вы говорите. Многократный вызов одной и той же функции из асинхронного обратного вызова не приводит к накоплению стека, поэтому здесь также нет проблем для решения. Хотя на первый взгляд это выглядит как рекурсивный вызов, стек уже раскручен и исходная функция возвращена до того, как функция будет вызвана снова, поэтому с точки зрения наращивания стека это не совсем рекурсия. - person jfriend00; 16.01.2017
comment
@jakubinf - я добавил больше информации к своему вопросу об асинхронной рекурсии. - person jfriend00; 16.01.2017

То, что у вас есть, это не хвостовой звонок. Хвостовой вызов — это вызов функции, выполняемый как заключительное действие другой функции. Хвостовой рекурсивный вызов аналогичен, за исключением того, что функция вызывает сама себя.

Однако последнее действие вашего кода — n*fac(n-1), а не fac(n-1). Это не рекурсивный хвостовой вызов, потому что текущий стек все еще должен помнить n при вычислении рекурсивных вызовов, чтобы он знал, какие числа умножать.

Что вы можете сделать, так это вычислить эту информацию за 1 шаг до:

const fac = (n, result = 1) =>
  n === 1
    ? result
    : fac(n - 1, n * result);

console.log(fac(5)); // 120

Или с точки зрения вашего кода:

function fac(n, result = 1) {
  // base case
  if (n === 1) {
    return result;
  }

  // compute the next result in the current stack frame
  const next = n * result;

  // propagate this result through tail-recursive calls
  return fac(n - 1, next);
}

Вот трассировка стека из Chrome Canary:

Правильные вызовы Tail в Chrome Canary

person nem035    schedule 15.01.2017
comment
Ну, но это довольно интересно, console.trace может печатать кадры стека через текущее выполнение, которое вы сказали, оно печатает 1000 строк, когда я делаю fac(1000), используя предоставленный вами код. - person ; 16.01.2017
comment
И это означает, что он должен храниться в ОЗУ где-то, занимая бесполезное место, и если бы я сделал fac(1000000000), я бы, вероятно, использовал как минимум 4 ГБ ОЗУ, что очень неэффективно, поправьте меня, если я ошибаюсь. - person ; 16.01.2017
comment
Или трассировка стека ограничена только запоминанием последних 10 шагов? - person ; 16.01.2017
comment
Тогда какая польза от оптимизации хвостового вызова в вашем примере, если я все равно получаю трассировку стека, хранящуюся в ОЗУ? Чисто теоретически, надеюсь, практически он никогда не превысит 20 МБ. - person ; 16.01.2017
comment
Чтобы увидеть, действительно ли у вас есть оптимизация хвостовой рекурсии, вызовите fac(10) и установите точку останова внутри функции и посмотрите на трассировку стека, когда n уменьшится до 5, и посмотрите, есть ли у вас накопление стека или нет, посмотрев на текущий стек. . - person jfriend00; 16.01.2017
comment
Спасибо, это то, что мне нужно было знать, что это не весь кадр стека - person ; 16.01.2017
comment
Вы должны использовать "use strict"; в начале для правильного хвостового вызова (PCT); но даже если это так, когда я проверяю и ваш, и мой фрагмент в отладчике Chrome, стек вызовов растет. - person Redu; 19.01.2017
comment
@Redu use strict может не понадобиться, в зависимости от где выполняется код. Что касается роста стека вызовов, то либо вы не используете точный код, либо в вашей версии Chrome еще нет хвостовых вызовов. Пробовали ли вы использовать Canary? - person nem035; 19.01.2017
comment
@jakubinf Мне нужно исправиться, console.trace покажет вам правильный стек вызовов, а это означает, что если вы видели несколько кадров стека, то либо ваша версия Node не поддерживает PTC, либо вы указали неправильный флаг. Я приложил вывод из Chrome Canary. - person nem035; 19.01.2017
comment
Ну.. как я понял для PCT ( Правильные вызовы хвоста) use strict; кажется необходимым. Помимо отложенного моего кода (который должен быть TCO), я использую ваш код, и он все еще растет в стеке вызовов. В основном поэтому я был немного скептичен в своем ответе. Честно говоря, я никогда не видел, чтобы в ES6 TCO работало так, как должно быть. Возможно, это моя оплошность или что-то в этом роде, но я хотел бы знать, почему. Кстати, моя версия Chrome — Linux V55.0.2883.87 (64-разрядная версия), только что обновленная вчера или что-то в этом роде. - person Redu; 19.01.2017
comment
@Redu, что я имею в виду, зависит от того, где выполняется код, так это то, что ES6 автоматически применяет строгий режим в определенных местах (where в моем предыдущем комментарии ссылается на соответствующую часть спецификации). Если вы посмотрите на добавленный скриншот в моем ответе, вы увидите, что PTC работает в Chrome Canary. PTC все еще довольно новая функция, поэтому они скрыты за флагами и в канареечных сборках. - person nem035; 19.01.2017
comment
Хм, возможно, вы правы... но, похоже, Chrome Canary для Linux пока не существует. Я полагал, что PCT — это щелчок переключателя после ES6. Видимо не так... - person Redu; 19.01.2017

Я не уверен, что ваша рекурсивная функция имеет хвостовой вызов. Может быть, вы можете попробовать следующее;

"use strict";
var fac = (n, r = 1) => n === 1 ? r : (r *= n, fac(n-1,r));
console.log(fac(5));

person Redu    schedule 15.01.2017