【问题标题】:Define fix-point combinator in Continuation Passing Style在继续传递样式中定义定点组合器
【发布时间】:2016-06-25 09:40:31
【问题描述】:
  • fix-point combinators 是引入递归的非常有用的工具。
  • Continuation-Passing style 是一种 lambda 演算风格,其中函数永远不会返回。相反,您将程序的其余部分作为 lambda 参数传递给您的函数并继续执行它们。它使您可以更好地控制执行流程并更轻松地定义各种流程更改结构(循环、协程等......)

但是,我想知道您是否可以用另一种方式表达?我见过的所有 CPS 风格的语言都有一个明确的 FIX 构造来定义递归。

  • 是不是因为在没有FIX 的情况下无法在普通 CPS 中定义定点组合器(或类似的)?如果有,你知道这件事的证明吗?
  • 还是仅仅是因为打字问题?
  • 或者有可能,但由于某种原因不切实际?
  • 或者我根本没有找到现成的解决方案...?

我希望类似 Y-combinator 的 CPS 函数 CPSY 可以这样工作: 如果我定义一个 Y-ready CPS 函数,比如:

function Yready(this, return) = 
    return (lambda <args> . <body using 'this' as recursion>);

然后我会将它放入CPSY 以生成一个递归到自身的函数:

function CPSY(F, return) = ?????

CPSY(Yready,
    lambda rec . <body where 'rec' names 'lambda <args>' from above, but with the loop closed>
)

CPSY 应该是一个简单的延续传递风格的函数,它本身不依赖于任何递归。 Y-combinator 可以在没有内置递归的普通 lambda 演算中以这种方式定义。它能否以某种形式存在于 CPS 中?


再次澄清:我正在寻找一个类似组合器的函数CPSY

  • 将启用 CPS 函数的递归
  • 它的定义不依赖递归
  • 它的定义以连续传递样式给出(CPSY 的正文中的任何地方都没有返回 lambda)

【问题讨论】:

  • "... IOW,不以任何形式使用letrec,仅使用let(在Scheme 术语中)。"我相信这就是你的意思。有趣的问题...

标签: functional-programming scheme continuation-passing fixpoint-combinators


【解决方案1】:

让我们先推导出 CPS-Y 用于 lambda 演算中的正阶求值,然后将其转换为应用阶。

Wikipedia page 通过以下等式定义定点组合子 Y:

Y f = f (Y f)

在 CPS 形式中,这个等式看起来更像这样:

Y f k = Y f (λh. f h k)

现在,考虑以下 Y 的非 CPS 正态定义:

Y f = (λg. g g) (λg. f (g g))

将其转换为 CPS:

Y f k = (λg. g g k) (λg.λk. g g (λh. f h k))

现在,对这个定义进行几次 beta-reduce,以检查它是否确实满足上面的“CPS 定点”条件:

Y f k = (λg. g g k) (λg.λk. g g (λh. f h k))
      = (λg.λk. g g (λh. f h k)) (λg.λk. g g (λh. f h k)) k
      = (λg.λk. g g (λh. f h k)) (λg.λk. g g (λh. f h k)) (λh. f h k)
      = Y f (λh. f h k)

瞧!


现在,对于应用顺序评估,当然,我们需要稍微改变一下。这里的推理与非 CPS 情况相同:我们需要“重击”递归的 (g g k) 调用,并仅在下次调用时继续:

Y f k = (λg. g g k) (λg.λk. f (λx.λk. g g (λF. F x k)) k)

以下是 Racket 的直接翻译:

(define (Y f k)
  ((λ (g) (g g k))
   (λ (g k) (f (λ (x k) (g g (λ (F) (F x k)))) k))))

我们可以检查它是否确实有效——例如,这是 CPS 中的递归三角数计算(为简单起见,算术运算除外):

(Y (λ (sum k) (k (λ (n k) (if (< n 1)
                              (k 0)
                              (sum (- n 1) (λ (s) (k (+ s n))))))))
   (λ (sum) (sum 9 print)))
;=> 45

我相信这回答了这个问题。

【讨论】:

    【解决方案2】:

    TL;DR:相同的应用顺序 Y 适用于以连续柯里化风格编写的 CPS 函数。


    在组合风格中,带有Y的阶乘的通常定义当然是

    _Y (\r -> \n -> { n==0 -> 1 ; n * r (n-1) })     , where
                                   ___^______
    _Y = \g -> (\x-> x x) (\x-> g (\n-> x x n))  -- for applicative and normal order
    

    CPS 阶乘定义为

    fact = \n k -> equals n 0         -- a conditional must expect two contingencies
                     (\True -> k 1) 
                     (\False -> decr n 
                                     (\n1-> fact n1          -- *** recursive reference
                                                 (\f1-> mult n f1 k)))
    

    CPS-Y 增加了额外的 contingency 参数(我说“偶然性”是为了消除与真正延续的歧义)。在方案中,

    (define (mult a b k)     (k (* a b)))
    (define (decr c   k)     (k (- c 1)))
    (define (equals d e s f) (if (= d e) (s 1) (f 0)))
    
    (((lambda (g) 
         ( (lambda (x) (x x))
           (lambda (x) (g (lambda (n k) ((x x) n k))))))
    
      (lambda (fact)
        (lambda (n k)
          (equals n 0 (lambda (_) (k 1))
                      (lambda (_) (decr n 
                                    (lambda (n1) (fact n1
                                                   (lambda (f1) (mult n f1 k))))))))))
    
      5 (lambda (x) (display x)) )
    

    This returns 120.

    当然,在自动柯里化惰性语言(但没有类型化!)中,通过 eta 收缩,上述 CPS-Y 与常规 Y 本身完全相同

    但是,如果我们的递归函数有两个实际参数,以及 continuation ⁄ contingency — 第三个呢?在类似于 Scheme 的语言中,我们是否必须有另一个 Y,里面有 (lambda (n1 n2 k) ((x x) n1 n2 k))

    我们可以切换到总是有应急参数first,并且总是以柯里化的方式编码(每个函数只有一个参数,可能产生另一个这样的函数,或者在所有应用后的最终结果)。还有it works

    (define (mult   k)   (lambda (x y) (k (* x y))))
    (define (decr   k)   (lambda (x)   (k (- x 1))))
    (define (equals s f) (lambda (x y) (if (= x y) (s) (f))))
    
    ((((lambda (g)                                ; THE regular,
         ( (lambda (x) (x x))                        ; applicative-order
           (lambda (x) (g (lambda (k) ((x x) k))))))   ; Y-combinator
    
       (lambda (fact)
        (lambda (k)
          (lambda (n)
            ((equals  (lambda () (k 1))
                      (lambda () ((decr (lambda (n1) 
                                            ((fact 
                                                (lambda (f1) ((mult k) n f1)))
                                             n1)))
                                   n)))
              n 0)))))
    
       (lambda (x) (display x))) 
      5)
    

    to type such a thing 也有方法,如果您输入了您的语言。或者,在无类型语言中,我们可以将所有参数打包到一个列表中。

    【讨论】:

    • 如果我错了,请纠正我,但我认为您谈论的是使用 Y-combinator for CPS 函数。我要求一个组合器本身在 in CPS 中定义。 \g -&gt; (\x-&gt; x x) (\x-&gt; g (\n k -&gt; (x x) n k)) 不是 CPS 函数。
    • 是的,我也在考虑这个问题。但是,是什么阻止我们定义(CPSY k) f = k (Y f)
    • 我的意思是,您的假设语言是否允许使用 lambda? lambda 函数直接返回其结果,而不是通过延续。还是您的语言禁止使用 lambda?我认为这里需要更多细节;正如你所问的那样,你的问题在这方面有点含糊。您谈到“CPS 语言”。我连这样一种语言都不熟悉;我熟悉 CPS 编程技术,CPS 编程学科...... AFAIK 任何功能都可以机械地转换为 CPS;看来 Y 也可以。
    • 认为这是一个与任何特定编程语言无关的理论问题(这就是为什么我最初没有放置“方案”标签的原因)。你有:只有 CPS-lambdas、常量、原始操作符(如上面定义的 multdecr),以及用于终止程序的 exit。我明确地说“CPSY 应该是一个简单的延续传递样式函数”。使用完整的 lambda 使问题变得有点简单:正如您自己发现的那样,常规 Y 可用于 CPS 函数。毕竟,CPS 函数只是普通函数的一个特例。
    • 为什么Y 的机械转换也不起作用?稍后会检查它...
    【解决方案3】:

    连续传递风格的匿名递归可以通过以下方式完成(使用JS6作为语言):

    // CPS wrappers
    const dec = (n, callback)=>{
        callback(n - 1)
    }
    const mul = (a, b, callback)=>{
        callback(a * b)
    }
    const if_equal = (a, b, then, else_)=>{
        (a == b ? then : else_)()
    }
    
    // Factorial
    const F = (rec, n, a, callback)=>{
        if_equal(n, 0,
            ()=>{callback(a)},
            ()=>{dec(n, (rn)=>{
                mul(a, n, (ra)=>{
                    rec(rec, rn, ra, callback)
                })
            })
        })
    }
    
    const fact = (n, callback)=>{
        F(F, n, 1, callback)
    }
    
    // Demo
    fact(5, console.log)
    

    为了摆脱标签F的双重使用,我们可以使用这样的实用函数:

    const rec3 = (f, a, b, c)=>{
        f(f, a, b, c)
    }
    const fact = (n, callback)=>{
        rec3(F, n, 1, callback)
    }
    

    这允许我们内联F:

    const fact = (n, callback)=>{
        rec3((rec, n, a, callback)=>{
            if_equal(n, 0,
                ()=>{callback(a)},
                ()=>{dec(n, (rn)=>{
                    mul(a, n, (ra)=>{
                        rec(rec, rn, ra, callback)
                    })
                })
            })
        }, n, 1, callback)
    }
    

    我们可以继续内联 rec3 以使 fact 自包含:

    const fact = (n, callback)=>{
        ((f, a, b, c)=>{
            f(f, a, b, c)
        })((rec, n, a, callback)=>{
            if_equal(n, 0,
                ()=>{callback(a)},
                ()=>{dec(n, (rn)=>{
                    mul(a, n, (ra)=>{
                        rec(rec, rn, ra, callback)
                    })
                })
            })
        }, n, 1, callback)
    }
    

    以下 JavaScript 使用相同的方法来实现 for 循环。

    const for_ = (start, end, func, callback)=>{
        ((rec, n, end, func, callback)=>{
            rec(rec, n, end, func, callback)
        })((rec, n, end, func, callback)=>{
            func(n, ()=>{
                if_equal(n, end, callback, ()=>{
                    S(n, (sn)=>{
                        rec(rec, sn, end, func, callback)
                    })
                })
            })
        }, start, end, func, callback)
    }
    

    这是我制作的完全异步 FizzBu​​zz 的一部分 https://gist.github.com/Recmo/1a02121d39ee337fb81fc18e735a0d9e

    【讨论】:

      【解决方案4】:

      这是“简单的解决方案”,而不是 OP 想要的非递归解决方案——它留作比较。


      如果您有一种提供递归绑定的语言,您可以为 CPS 函数定义 fix(这里使用 Haskell):

      Prelude> let fixC f = \c -> f (fixC f c) c
      Prelude> :t fixC
      fixC :: (t -> t1 -> t) -> t1 -> t
      Prelude> let facC' = \rec c n -> if n == 0 then c 1 else c (n * rec (n-1))
      Prelude> let facC = fixC facC'
      Prelude> take 10 $ map (facC id) [1..10]
      [1,2,6,24,120,720,5040,40320,362880,3628800]
      

      也许将fixC 作为原语提供可能会影响性能(如果您将延续表示为不仅仅是闭包),或者是由于“传统” lambda 演算没有可以使用的函数名称递归。

      (可能还有一个更有效的变体,类似于fix f = let x = f x in x。)

      【讨论】:

      • 递归绑定充当内置修复组合器。有些语言隐藏了它,有些语言对此更明确(例如let vs letrec)。但是,我正在寻找一种不依赖任何形式的内置 FIX 的解决方案。 Y-combinator 可以用普通的 lambda 演算以及模拟演算的实际语言来定义。但它可以在 CPS 中完成吗?
      • 编辑了这个问题,让自己更清楚:)
      • @CygnusX1 啊,现在我知道你想要什么了。这当然更有趣。不过,我会留下答案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-17
      • 2012-07-04
      • 2021-10-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多