这种延续传递方式Clojure函数发生器如何工作?

时间:2014-01-31 10:32:24

标签: clojure continuations continuation-passing

这是来自Clojure的Joy,第2版。 http://www.manning.com/fogus2/

 (defn mk-cps [accept? kend kont] 
   (fn [n] 
     ((fn [n k] 
        (let [cont (fn [v] (k ((partial kont v) n)))] 
          (if (accept? n) 
            (k 1) 
            (recur (dec n) cont)))) 
      n kend))) 

然后做一个阶乘:

(def fac (mk-cps zero? identity #(* %1 %2)))

我的理解:

  • mm-cps生成一个接收n的函数, fn [n]
  • 内部函数 fn [n k] 最初使用 n kend
  • 进行调用
  • 延续函数 cont [v] 定义为(调用 k 部分应用 kont v )作为第一个参数, n 作为第二个参数。为什么要使用partial而不是简单(k (cont v n))
  • 来编写
  • 如果accept?函数通过,则完成递归,将k应用于1.
  • 否则,recur将使用递减函数重复返回 fn [n k] ,并使用延续函数。
  • 一直以来, kont 都不会改变。

我是否正确k直到最后(k 1)才真正执行? 因此,在评估之前,(fac 3)会先扩展到(* 1 (* 2 3))

1 个答案:

答案 0 :(得分:15)

我没有这本书,但我认为激励的例子是

(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

继续传递风格的作用是将其内化。在递归调用返回之后,不是应用当前上下文/继续的剩余内容,而是将上下文/继续传递到递归调用以在完成时应用。我们不是通过函数组合明确地将它们作为调用帧存储在堆栈中而是隐式存储它们。

在这种情况下,我们在我们的factorial中添加了一个额外的参数k,这个函数可以完成我们在递归调用返回后所做的事情。

(defn fact-nk [n k]
  (if (zero? n)
      (k 1)
      (recur (dec n) (comp k (partial * n)))))

第一个k in是最后一个。最后,我们只想返回计算出的值,因此第一个k in应该是标识函数。

以下是基本案例:

(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

现在为了使这更灵活一点,我们可以将zero?*分解出来并改为使用变量参数。

第一种方法是

(defn cps-anck [accept? n c k]
  (if (accept? n)
      (k 1)
      (recur accept?, (dec n), c, (comp k (partial c n)))))

但是由于accept?c没有改变,我们可以解除然后再重新使用内部匿名函数。 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