Какой хороший способ переписать эту функцию без хвостовой рекурсии?

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

Clojure представляет собой интересный случай, поскольку в нем нет оптимизации хвостового вызова, а только хвостовая рекурсия через специальную форму "recur". Это также настоятельно не рекомендует использовать изменяемое состояние. В нем много ленивых конструкций, включая tree-seq, но я не вижу, как они могут мне помочь. для этого случая. Может ли кто-нибудь поделиться некоторыми методами, которые они почерпнули из C, Scheme, Haskell или других языков программирования?

(defn flatten [x]
  (let [type (:type x)]
    (cond (or (= type :NIL) (= type :TEXT))
          x
          (= type :CONCAT)
          (doc-concat (flatten (:doc1 x))
                      (flatten (:doc2 x)))
          (= type :NEST)
          (doc-nest (:level x)
                    (flatten (:doc x)))
          (= type :LINE)
          (doc-text " ")
          (= type :UNION)
          (recur (:doc1 x)))))

редактировать: По запросу в комментариях...

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

(define (frob x)
  (cond ((foo? x)
         x)
        ((bar? x)
         (macerate (f x) (frob (g x))))
        ((thud? x)
         (frobnicate (frob (g x))
                     (frob (h x))))))

Я выбрал раздражающие имена, чтобы показать, что я ищу ответы, которые не полагаются на алгебраические свойства x, macerate, frobnicate, f, g или h. Я просто хочу переписать рекурсию.

ОБНОВЛЕНИЕ:

Рич Хики любезно добавил в Clojure явную функцию батута.


person Steven Huwig    schedule 24.11.2008    source источник
comment
я бы сказал C++, но это было бы бесполезно ;-)   -  person Steven A. Lowe    schedule 25.11.2008
comment
было бы легче помочь, если бы вы объяснили, что делает функция. возможно, также используя более распространенный диалект LISP, я сам люблю использовать Scheme дома, но мне действительно не нравится внешний вид clojure   -  person Javier    schedule 25.11.2008
comment
Я добавил схему вопроса, но сомневаюсь, что это вам сильно поможет. :)   -  person Steven Huwig    schedule 25.11.2008
comment
используйте стиль передачи продолжения, тогда он выделяется в куче, а не в стеке! :)   -  person apg    schedule 25.11.2008
comment
CPS требует оптимизации хвостового вызова. :(   -  person Steven Huwig    schedule 25.11.2008
comment
Fee Fie Foe Fum - чую мусорные имена Джерри Суссмана :)   -  person Mike Dunlavey    schedule 25.11.2008
comment
CPS на самом деле не требует оптимизации хвостовых вызовов, но для больших преобразований вы определенно достигнете уровня рекурсии.   -  person apg    schedule 25.11.2008


Ответы (7)


Пожалуйста, не минусуйте это, потому что это некрасиво. Я знаю, что это некрасиво. Но это способ сделать это в стиле батута (без переполнения системного стека) и без использования gotos.

push x,1 on homemade stack
while stack length > 1
  n = pop
  if (n==1)
    x = pop
    if (type(x)==NIL || type(x)==TEXT)
      push x // this is the "return value"
    else if (type(x)==CONCAT)
      push 2 // say call doc-concat
      push doc2(x), 1 // 2nd recursion
      push doc1(x), 1 // 1st recursion
    else if (type(x)==NEST)
      push 3 // say call doc-nest
      push level(x) // push level argument to doc-nest
      push doc(x), 1 // schedule recursion
    else if (type(x)==LINE)
      push " " // return a blank
    else if (type(x)==UNION)
      push doc1(x), 1 // just recur
  else if (n==2)
    push doc-concat(pop, pop) // finish the CONCAT case
  else if (n==3)
    push doc-nest(pop, pop) // finish the NEST case
  endif
endwhile
// final value is the only value on the stack
person Mike Dunlavey    schedule 25.11.2008
comment
Вероятно, вам придется немного подправить его. - person Mike Dunlavey; 25.11.2008

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

я бы сказал, что у вас есть три альтернативы:

  1. totally reformulate the algorithm (that's what the Fibonacci examples do).
    • combine all functions into a single one with lots of cond's (ugly, and maybe won't result in a real tail-recursion, even with a single function).
    • выверните поток наизнанку: напишите одну простую хвостовую рекурсивную функцию, которая преобразует входные данные в последовательность операций, которые необходимо выполнить, а затем оцените ее.
person Javier    schedule 24.11.2008

Если flatten вызывает себя дважды (в случае :CONCAT), как его можно превратить в цикл? Может быть, я что-то упускаю. Кажется, это по своей сути прогулка по дереву.

Я имею в виду, что есть способы обхода дерева без стека, но что-то должно быть неограниченным, например, если вы делаете это с помощью FIFO или, как было предложено, с продолжениями.

person Mike Dunlavey    schedule 24.11.2008
comment
С неограниченной кучей все в порядке - это просто StackOverflowError из Java, которого я хочу избежать. - person Steven Huwig; 25.11.2008
comment
Что ж, в таком случае вы можете просто создать собственный стек из неограниченного списка и превратить свою процедуру в цикл. Если хотите, я псевдокодирую это в другом ответе. - person Mike Dunlavey; 25.11.2008

Стандартным общим приемом является преобразование в трамплинный стиль. Для вашей конкретной проблемы (реализация комбинаторов prettyprinting) вы можете найти полезную статью Дерека Оппена 1980 года "Prettyprinting" (не в Интернете, AFAIK). Он представляет основанный на стеке императивный алгоритм, аналогичный более позднему функциональному алгоритму Вадлера.

person Darius Bacon    schedule 25.11.2008
comment
Спасибо за цитаты. У меня есть подписка на ACM DL, и статья Оппена там. Немного лёгкого чтения на праздники... - person Steven Huwig; 25.11.2008

Вы можете использовать передачу продолжения:

(define (frob0 x k)
  (cond ((foo? x)
         (k x))
        ((bar? x)
         (frob0 (g x) 
           (lambda (y) 
             (k (macerate (f x) y))))
        ((thud? x)
         (frob0 (g x) 
           (lambda (y)
             (frob0 (h x) 
               (lambda (z)
                 (k (frobnicate y z))))))))

(define (frob x)
  (frob0 x (lambda (y) y))

Это не облегчит понимание :-(

person Chris Conway    schedule 24.11.2008
comment
Ага, опечатка. Спасибо, что указали на это. CPS требует оптимизации хвостового вызова, чтобы работать таким образом — вызовы (k ...) могут складываться. Clojure не выполняет совокупную стоимость владения. - person Steven Huwig; 25.11.2008
comment
... кроме как заставить больше людей жаловаться на отсутствие совокупной стоимости владения в JVM. :) - person Steven Huwig; 25.11.2008
comment
Но, как указано в другом комментарии, вы можете использовать стиль Trampolined. Это будет медленнее, так как вы, скорее всего, будете использовать для этого исключения, но это сработает. - person apg; 25.11.2008

Лучшее, что я могу придумать, это что-то вроде этого:

(define (doaction vars action)
  (cond ((symbol=? action 'frob)
         (cond ((foo? (first vars))
                (first vars))
               ((bar? (first vars))
                (doaction (list (f (first vars)) (doaction (g x) 'frob)) 'macerate)
etc...

Это не полностью хвостовая рекурсия, но, вероятно, лучшее, что вы можете получить. TCO – это действительно правильный путь. (Я понимаю, что Clojure не может иметь его из-за JVM).

person Claudiu    schedule 25.11.2008

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

(в коде Haskellish):

data Tree = Null | Node Tree Val Tree

-- original, non-tail-recursive function: flatten :: Tree -> Result flatten Null = nullval flatten (Node a v b) = nodefunc (flatten a) v (flatten b)

-- modified, tail-recursive code: data Task = A Val Tree | B Result Val

eval :: Tree -> [Task] -> Result use :: Result -> [Task] -> Result

eval Null tasks = use nullval tasks eval (Node a v b) tasks = eval a ((A v b):tasks)

use aval ((A v b):tasks) = eval b ((B aval v):tasks) use bval ((B aval v):tasks) = use (nodefunc aval v bval) tasks use val [] = val

-- actual substitute function flatten2 :: Tree -> Result flatten2 tree = eval tree []

person comingstorm    schedule 26.11.2008