Задний план
Вы когда-нибудь написали факториал
функция? Если у вас есть, то вы могли бы сделать что-то вроде:
function factorial(n) { let result = n; for (let i = n - 1; i > 1; i++) { result = result * i; } return result; }
Или даже что-то вроде:
function factorial(n) { return a > 1 ? n * factorial(n - 1) : 1; }
Оба являются действительными подходами, но есть что-то о втором подходе, которое облегчает понимание того, что он делает, мы можем легко прочитать, что факториал
Есть ли N * (N - 1)!
И что это называет себя до n
равен 1.
И тогда мы закончим, это то, что мы называем рекурсивной функцией:
Функция, которая либо называет себя или находится в потенциальном цикле вызовов функций.
Эта проблема
Рекурсия отличная, это помогает нам написать более лаконичный, читаемый и простой код. Тем не менее, есть большой недостаток в отношении рекурсии, предпринять, например, наши факториал
Функция, когда мы называем факториал (5)
Мы получаем 120
Однако, если мы назовем ту же функцию с возможностью большего значения, скажем 12 000
Мы получаем совершенно другой результат:
UnseError: превышен максимальный размер стека вызова
Видите ли, каждое время выполнения имеет максимальный размер стека (узел имеет ограничение вокруг 11k), поэтому, когда мы делаем длительные рекурсивные циклы, наша программа вылетает, так как больше нет места стека.
Решение
К счастью, есть альтернатива, которая позволяет нам безопасно писать рекурсивные функции, Оптимизация вызова хвоста Отказ
TCO – это процесс, который реализовал многие языки для решения длительных рекурсивных цепей. Он основан на предпосылке, что, когда процедура/функция вызывает подпрограмму в качестве его окончательного действия, то можно заменить текущий кадр стека вызовов с рамкой нового вызова, следовательно, быть в качестве исполнении в качестве петлевой версии этого функция.
Итак, как бы мы изменим наш факториал
Функция для выполнения этого ограничения? Мы могли бы сделать следующее:
function factorial(n, acc = 1) { return n > 1 ? factorial(n - 1, n * acc) : acc; }
Как вы видите, мы добавили свойство, ACC
, что позволяет нам передавать какую-либо соответствующую информацию (наш текущий накопительный продукт) к следующему факторному вызову, таким образом, оказание всей информации предыдущего вызова бесполезного и позволяя нам избавиться от этого кадра стека, поэтому вместо того, чтобы иметь 11k + Кадры стека, мы бы заменили ту же рамку 11k + раз.
Довольно аккуратно правильно?
К сожалению, хотя TCO является частью спецификации JavaScript, многие двигатели решили не реализовать его.
Интересная альтернатива
Несмотря на это, все еще есть безопасный способ использовать рекурсию. Мы можем реализовать нашу собственную версию TCO.
Согласно тому, что мы видели из TCO, нашей целью должно быть способствовать рекурсивным функциям для вести себя так, чтобы вместо того, чтобы иметь линейный рост размера стека, мы сохраняем постоянный размер Так что давайте спросим себя, какую структуру контроля мы знаем, что ведет себя таким образом? Петли! Так что, если у нас была петля, которая выполнила функции повторяющихся? Ну, это то, что мы называем батут .
Батут – это особый вид петли, который выполняет Танк-функции То есть функции, которые возвращают следующую функцию для вызова. Итак, что если мы конвертировали каждое из наших рекурсивных звонков в Thunk и передайте его на батуте? Будет ли наш стек поддерживать постоянный размер? Давайте посмотрим:
Во-первых, мы должны переписать нашу факториальную функцию, чтобы быть тщательной функцией, которая была бы чем-то вроде:
function factorial(n, ret = res => res) { return n > 1 ? () => factorial(n - 1, res => ret(n * res)) : ret(1); }
Давайте проанализируем, что мы там сделали, будем ли мы?
- Мы добавили аргумент на подпись функции,
рент
, который, как вы видите, это функция, которая выполняет особую роль, это позволяет нам составлять наши Thunks. - Теперь мы возвращаем функцию вместо стоимости факториального вычисления, делая то, что мы намерены отложить выполнение этой функции, пока наш батут не решит его вызвать.
Итак, давайте попадаем в нашу реализацию батута.
Пока мы сказали, что батут – это цикл, который выполняет Tamunted-функции по одному за раз, поэтому, воспользовавшись рисунком декоратора, мы могли бы написать следующее:
function trampoline(fn) { return function(...args) { let result = fn(...args); while (result && typeof result === 'function') { result = result(); } return result; }; }
Поскольку вы понимаете, что реализация довольно проста, мы украсим нашу рекурсивную функцию с нашим батутом, чтобы сделать TCO. Есть что-то, что стоит заметить здесь:
-
в то время как
бежит до тех пор, пока нет большеФункции
звонить. - Наше
FN
Параметр используется только в начале, так как каждый результат представляет следующийФункция
звонить.
Так что наш конечный результат будет чем-то вроде:
Как вы видите, наш стек вызовов никогда не шаги проходят 13
Рамы, которые позволяют нам работать с более длинными рекурсивными цепями, не беспокоясь о переполнении стека.
Немного допрос
Хотя …| батут Функция работает красиво, я все равно добавил что-то еще нашему API, а
Символ Действительно Да, одна из тех новых вещей с ES6, которые позволяют нам делать метапрограммирование, поэтому моя окончательная реализация будет:
function factorial(n, ret = res => res) { return n > 1 ? { fn: () => factorial(n - 1, res => ret(n * res)), [Symbol.for('recurse')]: true } : ret(1); } function trampoline(fn) { return function(...args) { let result = fn(...args); while (result && result[Symbol.for('recurse')]) { result = result.fn(); } return result; }; } // Or with Decorator syntax @trampoline function factorial(n, ret = res => res) { // ... }
Таким образом, мы можем быть уверены, что мы остановимся, когда мы должны, не после этого.
Финал
Рекурсия велика, одна из столпов функционального декларативного программирования, однако, имеет интересный недостаток, который может вызвать некоторые непреднамеренные проблемы. Здесь мы увидели, как оптимизировать рекурсивный звонок, используя хвостовые звонки. Также важно отметить, что, путем создания пути выполнения более сложным производительностью (временным мудрым) уменьшается, поэтому используйте этот метод с рассмотрением и избегайте добавления дополнительных слоев сложности, где не требуется.
Я надеюсь, что вы найдете эту статью полезную, пожалуйста, дайте мне знать, что вы думаете об этой реализации TCO.
слияние При проведении исследования в этой теме я наткнулся на Это Удивительная статья, которая упоминает возможность достижения аналогичного эффекта с использованием генераторов Python, поэтому я буду исследовать способ использования генераторов ES6 для улучшения того, как мы оптимизируем наши рекурсивные звонки.
Оригинал: “https://dev.to/pichardoj/do-you-even-recurse-and-if-you-do-do-you-do-it-safely-4mef”