我正在阅读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本身更有问题。
答案 0 :(得分:7)
不,Haskell代码不是尾递归的。它被保护 - 递归,递归由一个惰性数据构造函数:
(最终转换为++
调用)保护,其中由于懒惰只有一部分反过来探索递归调用树(a ++ b ++ c
),因此堆栈的深度永远不会超过 n ,即磁盘数量。这是非常小的,如7或8.
因此,Haskell代码探索a
,将c
部分放在一边。另一方面,您的Clojure代码会计算两个部分(a
和c
,因为b
不会计算)在连接之前,所以双递归,即计算量很大。
您正在寻找的不是TCO,而是TRMCO - tail recursion modulo cons优化,即从具有模拟堆栈的循环内部以自上而下的方式构建列表。 Clojure特别适合这种情况,尾部附加conj
(对吧?)而不是Lisp&和Haskell的头部前置cons
。
或者只是打印移动而不是构建所有移动列表。
编辑:实际上,TRMCO意味着如果我们维持"继续堆栈"我们可以重新使用调用帧。我们自己,所以堆栈深度正好 1 。在这种情况下,Haskell很可能构建一个嵌套++
thunk节点的左加深树,如here所述,但在Clojure中我们允许将其重新排列为右嵌套 list 我们自己,当我们维护自己的 to-do-next 调用描述堆栈时(b
和c
a ++ b ++ c
部分表达)。