https://habr.com/ru/post/464915/Привет, читатель.
Иногда для решении задачи приходится использовать
Рекурсию, в которой есть свои плюсы и минусы. Я столкнулся с проблемой переполнения стека.
Максимальная глубина рекурсии ограничена движком JavaScript. Точно можно рассчитывать на 10000 вложенных вызовов, некоторые интерпретаторы допускают и больше, но для большинства из них 100000 вызовов – за пределами возможностей. Существуют автоматические оптимизации, помогающие избежать переполнения стека вызовов («оптимизация хвостовой рекурсии»), но они ещё не поддерживаются везде и работают только для простых случаев.
Пример рекурсивной функции:
function sum(n) {
return n === 0 ? 0 : n + sum(n - 1)
}
sum(5) // 1 + 2 + 3 + 4 + 5 = 15
sum(100000) // Error: Maximum call stack size exceeded.
Хвостовая рекурсия позволяет оптимизировать вызовы компилятором и уже есть в стандарте
ES6, но поддержка браузерами
оставляет желать лучшего.
Пример хвостовой рекурсивной функции:
function sum(number, s = 0){
return number === 0 ? s : sum(number - 1, s + number)
}
Но, без поддержки браузерами мы столкнемся с той же проблемой — переполнение стека. Можем попробовать использовать вместе с
Trampolining.
Напишем декоратор, который будет принимать измененную рекурсивную функцию(следующий шаг) и исполнять ее в цикле, без увеличения глубины вызовов.
function trampoline(fn) {
return function(...args) {
let result = fn.apply(fn.context, args)
while (typeof result === 'function') {
result = result()
}
return result
}
}
Теперь наша рекурсивная функция должна возвращать функцию, которая будет сразу исполняться декоратором. Таким способом, мы условно сделаем массив функций и исполним их по очереди. Но поскольку мы каждый раз возвращаем новую анонимную функцию, этот код будет работать несколько медленнее.
function sum(number, s = 0) {
return number === 0 ? s : () => sum(number - 1, s + number)
}
Используем:
const trampolineSum = trampoline(sum)
trampolineSum(100000) // 5000050000
Это моя первая статья. Я старался не пересказывать понятия и указывал ссылку на источники. Тема не уникальная и давно существует на англоязычных сайтах, к сожалению на русском не нашел.
Спасибо, за внимание. Хорошего дня :)
Update
Производительность:
#1 Рекурсия
Код #1function sum(n) {
return n === 0 ? 0 : n + sum(n - 1)
}
console.time("run")
sum(1000)
console.timeEnd("run")
Результат #1 Safari 12.1.2[Debug] run: 0.353ms
[Debug] run: 0.281ms
[Debug] run: 0.305ms
[Debug] run: 0.315ms
[Debug] run: 0.319ms
[Debug] run: 0.231ms
[Debug] run: 0.255ms
[Debug] run: 0.334ms
[Debug] run: 0.370ms
[Debug] run: 0.274ms
[Debug] run: 0.260ms
Результат #1 Google Chrome 78.0.3892.0run: 0.118896484375ms
run: 0.101806640625ms
run: 0.099853515625ms
run: 0.10205078125ms
run: 0.10302734375ms
run: 0.099853515625ms
run: 0.106201171875ms
run: 0.103759765625ms
run: 0.105224609375ms
run: 0.262939453125ms
run: 0.136962890625ms
run: 0.10107421875ms
run: 0.10302734375ms
#2 Итерация
Код #2function sum(n) {
let sum = 0
for (let i = 1; i <= n; i++) {
sum += i
}
return sum
}
console.time("run")
sum(1000)
console.timeEnd("run")
Результат #2 Safari 12.1.2[Debug] run: 0.562ms
[Debug] run: 0.552ms
[Debug] run: 0.502ms
[Debug] run: 0.527ms
[Debug] run: 0.434ms
[Debug] run: 0.462ms
[Debug] run: 0.511ms
[Debug] run: 0.528ms
[Debug] run: 0.549ms
[Debug] run: 0.475ms
[Debug] run: 0.530ms
[Debug] run: 0.494ms
Результат #2 Google Chrome 78.0.3892.0run: 0.932861328125ms
run: 0.751953125ms
run: 0.4580078125ms
run: 0.678955078125ms
run: 0.424072265625ms
run: 0.505859375ms
run: 0.563720703125ms
run: 0.404052734375ms
run: 0.411865234375ms
run: 0.634033203125ms
run: 0.4169921875ms
run: 0.390869140625ms
run: 0.464111328125ms
#3 Хвостовая рекурсия и «батут»
Код #3function trampoline(fn) {
return function(...args) {
let result = fn.apply(fn.context, args)
while (typeof result === 'function') {
result = result()
}
return result
}
}
function sum(number, s = 0) {
return number === 0 ? s : () => sum(number - 1, s + number)
}
const trampolineSum = trampoline(sum)
console.time("run")
trampolineSum(1000)
console.timeEnd("run")
Результат #3 Safari 12.1.2[Debug] run: 0.936ms
[Debug] run: 0.792ms
[Debug] run: 0.882ms
[Debug] run: 0.826ms
[Debug] run: 0.968ms
[Debug] run: 0.818ms
[Debug] run: 1.681ms
[Debug] run: 0.777ms
[Debug] run: 1.109ms
[Debug] run: 0.832ms
[Debug] run: 0.826ms
Результат #3 Google Chrome 78.0.3892.0run: 0.60888671875ms
run: 0.989990234375ms
run: 0.567138671875ms
run: 0.56005859375ms
run: 1.0087890625ms
run: 0.5400390625ms
run: 0.578125ms
run: 0.541015625ms
run: 0.557861328125ms
run: 1.97607421875ms
run: 0.570068359375ms
run: 0.593017578125ms
run: 0.530029296875ms
run: 0.89794921875ms
run: 0.590087890625ms
#4 Хвостовая рекурсия и «батут» (большое число)
Код #4function trampoline(fn) {
return function(...args) {
let result = fn.apply(fn.context, args)
while (typeof result === 'function') {
result = result()
}
return result
}
}
function sum(number, s = 0) {
return number === 0 ? s : () => sum(number - 1, s + number)
}
const trampolineSum = trampoline(sum)
console.time("run")
trampolineSum(100000)
console.timeEnd("run")
Результат #4 Safari 12.1.2[Debug] run: 33.693ms
[Debug] run: 24.564ms
[Debug] run: 25.313ms
[Debug] run: 23.262ms
[Debug] run: 24.848ms
[Debug] run: 23.909ms
[Debug] run: 24.248ms
[Debug] run: 32.416ms
[Debug] run: 24.090ms
[Debug] run: 23.986ms
Результат #4 Google Chrome 78.0.3892.0run: 40.73681640625ms
run: 33.955078125ms
run: 40.907958984375ms
run: 37.693115234375ms
run: 28.929931640625ms
run: 30.7548828125ms
run: 29.720947265625ms
run: 40.8310546875ms
run: 31.5830078125ms
run: 30.712890625ms
run: 30.162841796875ms
run: 31.56103515625ms
По результатам мы видим, что наш декоратор хоть и позволяет избежать ошибки переполнение стека, но работает он медленнее чем рекурсивный и итеративный вариант. Так что данный способ стоит использовать только если вы не можете заменить рекурсию на итерацию или боитесь переполнение стека при выполнении вашей рекурсивной функции.