【问题标题】:How does this continuation-passing style Clojure function generator work?这个延续传递风格的 Clojure 函数生成器是如何工作的?
【发布时间】:2026-01-02 06:25:02
【问题描述】:

这是来自 Clojure 的喜悦,第 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] 最初是用 nkend 调用的
  • 延续函数 cont [v] 定义为(调用 k 部分应用 kontv) 作为第一个参数,n 作为第二个参数。为什么要使用partial 而不是简单的(k (cont v n)) 来编写?
  • 如果accept? 函数通过,则结束递归,将k 应用于1。
  • 否则,recur 会以递减的 n 和延续函数返回到 fn [n k]
  • 自始至终,kont没有改变。

k 直到最后的(k 1) 才真正执行,这对吗? 因此,(fac 3) 在被评估之前首先扩展为 (* 1 (* 2 3))

【问题讨论】:

    标签: clojure continuations continuation-passing


    【解决方案1】:

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

    (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
    

    现在为了更灵活一点,我们可以将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
    

    【讨论】:

    • 感谢您的详细说明!我认为这本书在这方面有太多的“魔力”。