Возможно, у ваших реальных проблем есть простые решения, позволяющие избежать большого потребления стека, поэтому, пожалуйста, не стесняйтесь добавлять детали. Однако, без дополнительных знаний о вашем конкретном коде, вот общий подход к сокращению потребления стека в рекурсивных программах, основанный на батутах и продолжениях.
Walker
Вот типичная рекурсивная функцияэто не тривиально хвостовая рекурсия, написанная на Common Lisp, потому что я не знаю F #:
(defun walk (form transform join)
(typecase form
(cons (funcall join
(walk (car form) transform join)
(walk (cdr form) transform join)))
(t (funcall transform form))))
Код, однако, довольно прост, и, как мы надеемся, обходится деревом, состоящим из клеток cons:
- если форма является cons-ячейкой, рекурсивно пройтись по машине (соответственно cdr) и присоединиться к результатам
- В противном случае применить преобразование к значению
Например:
(walk '(a (b c d) 3 2 (a 2 1) 0)
(lambda (u) (and (numberp u) u))
(lambda (a b) (if a (cons a b) (or a b))))
=> (3 2 (2 1) 0)
Код обходит форму и сохраняет только числа, но сохраняет (не пустое) вложение.
Вызов trace
на walk
с помощьюПриведенный выше пример показывает максимальную глубину 8 вложенных вызовов.
Продолжения и батут
Вот адаптированная версия, называемая walk/then
, которая просматривает форму, как и ранее, и когда результат доступен, свсе then
на это. Здесь then
- это продолжение .
Функция также возвращает thunk , то есть замыкание без параметров. Случается так, что когда мы возвращаем замыкание, стек разворачивается, и когда мы применяем thunk , он запускается из нового стека, но продвинулся в вычислениях (обычно я представляю, как кто-то поднимается по эскалатору). что идет вниз). Тот факт, что мы возвращаем thunk для уменьшения количества кадров стека, является частью trampoline .
Функция then
принимает значение, а именно результат, который в конечном итоге будет проходить текущий обходвернуть. Таким образом, результат передается вниз стека, а то, что возвращается на каждом шаге, является функцией thunk.
Вложенные продолжения позволяют захватить сложное поведение transform
/ join
,нажимая оставшиеся части вычисления во вложенных продолжениях.
(defun walk/then (form transform join then)
(typecase form
(cons (lambda ()
(walk/then (car form) transform join
(lambda (v)
(walk/then (cdr form) transform join
(lambda (w)
(funcall then (funcall join v w))))))))
(t (funcall then (funcall transform form)))))
Например, (walk/then (car form) transform join (lambda (v) ...))
выглядит следующим образом: обходит машину формы с аргументами transform
и join
, ив конце концов позвоните (lambda (v) ...)
о результате;а именно, пройдитесь по CDR, а затем объедините оба результата;в конце концов, вызовите then
для объединенного результата .
. Чего не хватает, так это способа непрерывного вызова возвращаемого thunk до исчерпания;вот это с циклом, но это легко может быть хвостовой рекурсивной функцией:
(loop for res =
(walk/then '(a (b c d) 3 2 (a 2 1) 0)
(lambda (u) (and (numberp u) u))
(lambda (a b) (if a (cons a b) (or a b)))
#'identity)
then (typecase res (function (funcall res)) (t res))
while (functionp res)
finally (return res))
Выше возвращается (3 2 (2 1) 0)
, и глубина трассировки никогда не превышает 2 при трассировке walk/then
.
См. статью Эли Бендерского , где можно найти еще один пример на Python.