我没有这本书,但我认为激励的例子是
(defn fact-n [n]
(if (zero? n)
1
(* n (recur (dec n)))))
;=> CompilerException: Can only recur from tail position
最后一种形式必须改为编写(* n (fact-n (dec n)))
,而不是尾递归。问题是在递归之后还有一些事情要做,即乘以n
。
延续传球风格所做的就是把它翻过来。在递归调用返回后,不要应用当前上下文/延续的剩余部分,而是将上下文/延续传递给递归调用以在完成时应用。我们不是在堆栈中隐式地将延续存储为调用帧,而是通过函数组合显式地累积它们。
在这种情况下,我们向阶乘添加了一个额外的参数k
,该函数执行我们在递归调用返回后会执行的操作。
(defn fact-nk [n k]
(if (zero? n)
(k 1)
(recur (dec n) (comp k (partial * n)))))
第一个k
进是最后一个出。最终这里我们只想返回计算出来的值,所以第一个k
应该是恒等函数。
这是基本情况:
(fact-nk 0 identity)
;== (identity 1)
;=> 1
这是n = 3
:
(fact-nk 3 identity)
;== (fact-nk 2 (comp identity (partial * 3)))
;== (fact-nk 1 (comp identity (partial * 3) (partial * 2)))
;== (fact-nk 0 (comp identity (partial * 3) (partial * 2) (partial * 1)))
;== ((comp identity (partial * 3) (partial * 2) (partial * 1)) 1)
;== ((comp identity (partial * 3) (partial * 2)) 1)
;== ((comp identity (partial * 3)) 2)
;== ((comp identity) 6)
;== (identity 6)
;=> 6
与非尾递归版本比较
(fact-n 3)
;== (* 3 (fact-n 2))
;== (* 3 (* 2 (fact-n 1)))
;== (* 3 (* 2 (* 1 (fact-n 0))))
;== (* 3 (* 2 (* 1 1)))
;== (* 3 (* 2 1))
;== (* 3 2)
;=> 6
现在为了让它更灵活一点,我们可以分解出 thezero?
和 the*
并让它们变成可变参数。
第一种方法是
(defn cps-anck [accept? n c k]
(if (accept? n)
(k 1)
(recur accept?, (dec n), c, (comp k (partial c n)))))
但是由于accept?
andc
没有改变,我们可以将 then 取出并递归到一个内部匿名函数。Clojure 对此有一个特殊的形式,loop
.
(defn cps-anckl [accept? n c k]
(loop [n n, k k]
(if (accept? n)
(k 1)
(recur (dec n) (comp k (partial c n))))))
最后我们可能想把它变成一个函数生成器来拉入n
.
(defn gen-cps [accept? c k]
(fn [n]
(loop [n n, k k]
(if (accept? n)
(k 1)
(recur (dec n) (comp k (partial c n)))))))
这就是我的写作方式mk-cps
(注意:最后两个论点颠倒了)。
(def factorial (gen-cps zero? * identity))
(factorial 5)
;=> 120
(def triangular-number (gen-cps #{1} + identity))
(triangular-number 5)
;=> 15