延续传递样式的中间值和返回值

时间:2016-07-27 11:29:31

标签: c# lambda scheme continuations continuation-passing

我来自OOP,非功能背景,因此我无法完全可视化有关continuation passing的几个在线示例。此外,像Scheme这样的函数式语言不必指定参数类型或返回值,所以我不确定我是否正确理解了这个想法。

由于C#支持lambdas,我从维基百科的文章中拿出了第一个例子并尝试将其移植到C#并进行强类型输入,以了解该模式将如何应用:

// (Scheme)

// direct function
(define (pyth x y)
 (sqrt (+ (* x x) (* y y))))

// rewriten with CPS 
(define (pyth& x y k)
 (*& x x (lambda (x2)
          (*& y y (lambda (y2)
                   (+& x2 y2 (lambda (x2py2)
                              (sqrt& x2py2 k))))))))

// where *&, +& and sqrt& are defined to 
// calculate *, + and sqrt respectively and pass the result to k
(define (*& x y k)
 (k (* x y)))

因此,在C#中重写CPS pyth&版本会导致:

// (C#6)

// continuation function signature
delegate double Cont(double a);

// *&, +& and sqrt& functions
static double MulCont(double a, double b, Cont k) => k(a * b);
static double AddCont(double a, double b, Cont k) => k(a + b);
static double SqrtCont(double a, Cont k) => k(Math.Sqrt(a));

// sqrt(x*x + y*y), cps style
static double PythCont(double x, double y, Cont k) =>
    MulCont(x, x, x2 =>
        MulCont(y, y, y2 =>
            AddCont(x2, y2, x2py2 =>
                SqrtCont(x2py2, k))));

我本可以使用泛型而不是double,但签名会更长。无论如何,我不确定的是:

  1. 上面的Cont签名是否正确(即Func<double, double>)?应该延续fn。接受参数,处理它,然后返回相同类型的值?

  2. 当我第一次开始阅读有关延续的内容时,我感觉这个延续函数将被调用到调用堆栈中的每一步,但在上面的示例中,它只被传递给{{1}所有其他的电话都会让lambdas真的没有通过&#34;中间值到原始延续。上面函数中的代码基本上类似于sqrt&,所以这意味着我对中间&#34;钩子的假设&#34;是错的?

2 个答案:

答案 0 :(得分:3)

  1. 是的,除非您想要对最外层的延续进行非数字化的操作,否则它是正确的。 当您的原始表达涉及更多类型时,您只需要更多的&#34; Cont&#34;

    (define(foo x)(if(= x 0)1 0))

  2. 在这种情况下,它可能看起来像这样(对不起,我为了简洁而在计划中写道):

    (define (foo& x k)
      (=& x 0 (lambda (r1)
                (if r1 (k 1) (k 0)))))
    

    - 现在最外面的延续有一个数字(让我们说是一个int)作为输入,而提供给&#34; =&amp;&#34;有bool-&gt; int类型。

    1. 你几乎是对的(达到二元性) - 调用堆栈上的每一步现在都是对某些延续的调用。 一般来说,您可能会混淆使用cps的第一类延续 - 前者是一种语言功能(如在方案中,您可以使用call / cc运算符访问当前延续),后者是一种可以在任何地方使用的技术。 你实际上可以将表达式转换为cps,甚至没有你的语言中的高阶函数(只是以某种方式表示它们)。
    2. 你问的另一件事是cps如何与控制流有关。好吧,请注意,在应用程序,函数式语言(如方案)中,您唯一指定的是在应用程序的情况下,首先评估操作数和运算符,然后将后者应用于前者。你评估操作数的顺序并不重要 - 你可以从左到右,从右到左[或者以某种疯狂的方式]进行操作。但是如果你不使用纯粹的功能风格,操作数会产生一些副作用呢?它们可能例如打印东西到stdout,然后返回一些值。在这种情况下,您希望控制订单。 如果我记得很清楚,使用gambit-C编译的程序会从右到左评估参数,而使用gambit的解释器从左到右进行解释 - 所以问题确实存在;)。然后cps可能会拯救你[实际上还有其他方法,但我们现在正在关于cps!]。 在方案示例中,您发布的是强制参数&#34; +&#34;从左到右进行评估。 您可以轻松改变:

      (define (pyth& x y k)
       (*& y y (lambda (y2)
                (*& x x (lambda (x2)
                         (+& x2 y2 (lambda (x2py2)
                                    (sqrt& x2py2 k))))))))
      

      这就是事情。

      在其他一些应用程序中,正如人们在评论中已经说过的那样,转换为CPS会将每个应用程序移动到尾部位置,因此调用堆栈正在被lambdas替换,而且如果你对它们进行去功能化,你得到的是数据表示控制流的结构 - 要转换为C语言或其他命令式语言的简洁形式。完全自动化! 或者,如果你想实施一些monad mumbo-jumbo,比如说Maybe monad,在CPS中它很容易,只是在每个延续前面 - lambda测试接收值是否为&#34; Just 东西&#34; (在这种情况下执行作业并将结果推送到续),或者#34; Nothing&#34;,在这种情况下,您只需按Nothing(到continuation-lambda)。 当然不是由另一个程序或宏,而不是手工,因为它可能是单调乏味的 - 最神奇的恐惧cps是它很容易自动转换为cps。

      希望我没有让它变得不必要地复杂化。

答案 1 :(得分:1)

我已经对Continuation monad进行了非常全面的介绍,您可以在这里Discovering the Continuation Monad in C#

您还可以找到.Net小提琴here

我在这里总结总结一下 从初始功能开始

int Square(int x ){return (x * x);}
  1. 使用回调并删除返回类型
    public static void Square(int x, Action<int> callback)
    {
    callback(x * x);
    }
  1. 促成回调
    public static Action<Action<int>> Square(int x)
    {
     return (callback) => { callback(x * x); };
    }
  1. 概括返回的Continuation
    public static Func<Func<int,T>,T> Square<T>(int x)
    {
         return (callback) => { callback(x * x); };
    }
  1. 提取延续结构,也称为monad的返回方法
       delegate T Cont<U, T>(Func<U, T> f);

    public static Cont<U, T> ToContinuation<U, T>(this U x)
    {
       return (callback) => callback(x);
    }

    square.ToContinuation<Func<int, int>, int>()
  1. 添加bind Monadic方法,从而完成Monad
    public static Cont<V, Answer> Bind<T, U, V, Answer>(
    this Cont<T, Answer> m,
    Func<T, Cont<U, Answer>> k, 
    Func<T, U, V> selector)
    {
     return (Func<V, Answer> c) => 
    m(t => k(t)(y => c(selector(t, y))));
    }