TCO在Clojure中优化了河内塔

时间:2016-11-03 17:04:46

标签: haskell recursion clojure tail-recursion towers-of-hanoi

我正在阅读Introdution to Haskell课程,他们正在引入着名的河内塔问题作为头等课程的作业。我受到诱惑并写了一个解决方案:

type Peg = String

type Move = (Peg, Peg)

hanoi :: Int -> Peg -> Peg -> Peg -> [Move]
hanoi n b a e
  | n == 1 = [(b, e)]
  | n > 1 = hanoi (n - 1) b e a ++ hanoi 1 b a e ++ hanoi (n - 1) a b e
  | otherwise = []

我已经玩了一点,看到它显然使用Tail Call Optimization,因为它在恒定的内存中工作。

Clojure是我大部分时间都在工作的语言,因此我遇到了编写Clojure解决方案的挑战。天真的被丢弃,因为我想写它来使用TCO:

(defn hanoi-non-optimized
  [n b a e]
  (cond
    (= n 1) [[b e]]
    (> n 1) (concat (hanoi-non-optimized (dec n) b e a)
                    (hanoi-non-optimized 1 b a e)
                    (hanoi-non-optimized (dec n) a b e))
    :else   []))

嗯,Clojure是JVM托管的,默认情况下没有TCO,应该使用recur来获取它(我知道故事......)。另一方面,recur强加了一些语法约束,因为它必须是最后一个表达式 - 必须是尾部。我感觉有点不好,因为我仍然无法在Haskell中编写一个简短/富有表现力的解决方案并同时使用TCO。

目前我还看不到一个简单的解决方案吗?

我非常尊重这两种语言,并且已经知道我的方法比使用Clojure本身更有问题。

1 个答案:

答案 0 :(得分:7)

不,Haskell代码不是尾递归的。它被保护 - 递归,递归由一个惰性数据构造函数:(最终转换为++调用)保护,其中由于懒惰只有一部分反过来探索递归调用树(a ++ b ++ c),因此堆栈的深度永远不会超过 n ,即磁盘数量。这是非常小的,如7或8.

因此,Haskell代码探索a,将c部分放在一边。另一方面,您的Clojure代码会计算两个部分(ac,因为b不会计算)连接之前,所以双递归,即计算量很大。

您正在寻找的不是TCO,而是TRMCO - tail recursion modulo cons优化,即从具有模拟堆栈的循环内部以自上而下的方式构建列表。 Clojure特别适合这种情况,尾部附加conj(对吧?)而不是Lisp&和Haskell的头部前置cons

或者只是打印移动而不是构建所有移动列表。

编辑:实际上,TRMCO意味着如果我们维持"继续堆栈"我们可以重新使用调用帧。我们自己,所以堆栈深度正好 1 。在这种情况下,Haskell很可能构建一个嵌套++ thunk节点的左加深树,如here所述,但在Clojure中我们允许将其重新排列为右嵌套 list 我们自己,当我们维护自己的 to-do-next 调用描述堆栈时(bc a ++ b ++ c部分表达)。