Я делал некоторый функциональный JavaScript. Я думал, что оптимизация Tail-Call была реализована, но, как оказалось, я ошибался. Таким образом, я должен был научить себя прыжкам на батуте . Немного почитав здесь и в других местах, я смог освоить основы и сконструировал свой первый батут:
/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/
function loopy(x){
if (x<10000000){
return function(){
return loopy(x+1)
}
}else{
return x;
}
};
function trampoline(foo){
while(foo && typeof foo === 'function'){
foo = foo();
}
return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};
alert(trampoline(loopy(0)));
Моя самая большая проблема, я не знаю, почему это работает. У меня появилась идея перезапустить функцию в цикле while вместо использования рекурсивного цикла. Кроме того, технически моя базовая функция уже имеет рекурсивный цикл. Я не запускаю базовую loopy
функцию, но я запускаю функцию внутри нее. Что мешает foo = foo()
вызвать переполнение стека? И foo = foo()
технически не мутирует, или я что-то упускаю? Возможно, это просто необходимое зло. Или какой-то синтаксис мне не хватает.
Есть ли способ понять это? Или это просто какой-то хак, который как-то работает? Я был в состоянии пройти через все остальное, но это меня озадачило.
loopy
не переполняется, потому что не вызывает себя .Ответы:
Причина, по которой ваш мозг восстает против этой функции,
loopy()
заключается в том, что она имеет противоречивый тип :Многие языки даже не позволяют вам делать подобные вещи или, по крайней мере, требуют гораздо большего набора текста, чтобы объяснить, как это должно иметь какой-то смысл. Потому что это действительно не так. Функции и целые числа - совершенно разные виды объектов.
Итак, давайте внимательно пройдемся по циклу while:
Первоначально
foo
равноloopy(0)
. Что такоеloopy(0)
? Ну, это меньше, чем 10000000, так что мы получаемfunction(){return loopy(1)}
. Это истинное значение, и это функция, поэтому цикл продолжается.Теперь мы пришли к
foo = foo()
.foo()
так же, какloopy(1)
. Так как 1 все еще меньше 10000000, возвращаетсяfunction(){return loopy(2)}
, который мы затем назначаемfoo
.foo
все еще функция, поэтому мы продолжаем ... пока в конечном итоге foo не будет равенfunction(){return loopy(10000000)}
. Это функция, поэтому мы делаем ещеfoo = foo()
один раз, но на этот раз, когда мы вызываемloopy(10000000)
, x не меньше 10000000, поэтому мы просто возвращаем x. Поскольку 10000000 также не является функцией, это также завершает цикл while.источник
Кевин лаконично указывает, как работает этот конкретный фрагмент кода (и почему он совершенно непонятен), но я хотел бы добавить некоторую информацию о том, как обычно работают батуты .
Без оптимизации хвостового вызова (TCO) каждый вызов функции добавляет кадр стека к текущему стеку выполнения. Предположим, у нас есть функция для вывода обратного отсчета чисел:
Если мы позвоним
countdown(3)
, давайте проанализируем, как будет выглядеть стек вызовов без TCO.При использовании TCO каждый рекурсивный вызов
countdown
находится в хвостовой позиции (ничего не остается, кроме как вернуть результат вызова), поэтому стековый кадр не выделяется. Без TCO стек взрывается даже при небольшом увеличенииn
.Trampolining обходит это ограничение, вставляя обертку вокруг
countdown
функции. Затемcountdown
не выполняет рекурсивные вызовы и вместо этого сразу возвращает функцию для вызова. Вот пример реализации:Чтобы лучше понять, как это работает, давайте посмотрим на стек вызовов:
На каждом шаге
countdownHop
функция отказывается от прямого контроля за тем, что происходит дальше, вместо этого возвращая функцию для вызова, которая описывает, что она хотела бы, чтобы происходило дальше. Функция батута затем берет это и вызывает его, затем вызывает любую функцию, которая возвращает, и так далее, пока не будет «следующего шага». Это называется trampolining, потому что поток управления «подпрыгивает» между каждым рекурсивным вызовом и реализацией trampoline, а не функцией, непосредственно повторяющейся. Оставляя контроль над тем, кто делает рекурсивный вызов, функция батута может гарантировать, что стек не станет слишком большим. Примечание: эта реализацияtrampoline
опускает возвращаемые значения для простоты.Может быть сложно узнать, хорошая ли это идея. Производительность может пострадать из-за каждого шага, выделяющего новое закрытие. Умные оптимизации могут сделать это жизнеспособным, но вы никогда не знаете. Прыжки на батуте в основном полезны для преодоления жестких ограничений рекурсии, например, когда языковая реализация устанавливает максимальный размер стека вызовов.
источник
Возможно, станет легче понять, если батут реализован с выделенным типом возврата (вместо злоупотребления функцией):
Сравните это с вашей версией
trampoline
, где случай рекурсии - это когда функция возвращает другую функцию, а базовый случай - когда она возвращает что-то еще.Он больше не называет себя. Вместо этого он возвращает результат (в моей реализации, буквально a
Result
), который передает, продолжить ли рекурсию или выйти из нее .Да, это точно необходимое зло цикла. Можно было бы написать и
trampoline
без мутации, но это потребовало бы рекурсии снова:Тем не менее, это показывает идею того, что функция батута делает еще лучше.
Смысл трамплинга - абстрагирование хвостового рекурсивного вызова от функции, которая хочет использовать рекурсию в возвращаемое значение, и выполнение реальной рекурсии только в одном месте -
trampoline
функции, которая затем может быть оптимизирована в одном месте для использования петля.источник
foo = foo()
является мутацией в смысле изменения локального состояния, но я бы обычно рассмотрел это переназначение, так как вы фактически не модифицируете базовый объект функции, вы заменяете его функцией (или значением), которую он возвращает.foo
содержит, только измененная переменная.while
Петля требует некоторого изменяемого состояния , если вы хотите, чтобы это прекратить, в этом случае переменнойfoo
илиx
.fn
в рекурсивный вызовtrampoline
- я не уверен, что это улучшение.