Intermediate and return values in continuation-pas

2019-07-17 23:33发布

I am coming from a OOP, non-functional background, so I am having trouble fully visualizing several online examples regarding continuation passing. Also, functional languages like Scheme don't have to specify types of arguments or return values, so I am unsure whether I got the idea correctly.

Since C# supports lambdas, I took the first example from the Wikipedia article and tried to port it to C# with strong typing, to see how the pattern would apply:

// (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)))

So, rewriting the CPS pyth& version in C# resulted in:

// (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))));

I could have used generics instead of double, but signatures would be longer. Anyway, what I am not sure is:

  1. Is the Cont signature above correct (i.e. Func<double, double>)? Should the continuation fn. accept the parameter, process it, and then return the value of the same type back?

  2. When I first started reading about continuations, I got the feeling that this continuation function will get invoked for each step in the call stack, but in the example above it's only passed to sqrt&, and all other calls get lambdas which don't really "pass" intermediate values to the original continuation. The code above in the function above is basically analogue to k(Math.Sqrt(x * x + y * y)), so does this mean my assumption about intermediate "hooks" is wrong?

2条回答
姐就是有狂的资本
2楼-- · 2019-07-18 00:10

I have created a very comprehensive introduction to the Continuation monad that you can Find Here Discovering the Continuation Monad in C#

Also you can find a.Net Fiddle here

I Repeat it in summary here Starting from an initial Function

int Square(int x ){return (x * x);}
  1. Use Callback and remove return type
    public static void Square(int x, Action<int> callback)
    {
    callback(x * x);
    }
  1. Curry the Callback
    public static Action<Action<int>> Square(int x)
    {
     return (callback) => { callback(x * x); };
    }
  1. Generalize the returned Continuation
    public static Func<Func<int,T>,T> Square<T>(int x)
    {
         return (callback) => { callback(x * x); };
    }
  1. Extract the Continuation Structure Also Known As the Return Method of the 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. Add The bind Monadic method and thus Complete the 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))));
    }

查看更多
萌系小妹纸
3楼-- · 2019-07-18 00:21
  1. Yes, unless you want to do anything non-numerical with the outermost continuation, it is correct. You would only need more "Cont"s when your original expression involves more types, e.g.

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

in which case it might look like this (sorry I write in scheme for brevity):

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

-- now the outermost continuation has a number (let's say an int) as input, while the one provided to "=&" has bool->int type.

  1. You are almost right (up to duality) -- each step on the call stack is now a call to some continuation. In general you might be confusing first-class continuations with cps -- the former is a language feature (as in scheme where you can access the current continuation with call/cc operator), the latter is a technique you can use anywhere. You actually can convert expressions to cps without even having higher-order functions in your language (by just representing them somehow).

Another thing you asked is how cps relates to control flow. Well, notice that in applicative, functional language (like scheme) the only thing you have specified is that in case of application, you first evaluate the operands and the operator, and then apply the latter to the former. It does not matter in what order you evaluate the operands -- you might do it left-to-right, right-to-left [or perhaps in some crazy way]. But what if you're not using purely functional style, and the operands cause some side effects? They might e.g. print something to stdout, and later return some value. In that case, you would like to have control over the order. If I remember well, programs compiled with gambit-C evaluate arguments right-to-left, while interpreted with gambit's interpreter left-to-right -- so the problem really exists ;). And precisely then the cps might save you [actually there are other means as well, but we're about cps right now!]. In the scheme example you posted it is forced that the arguments of "+" are evaluated left-to-right. You might alter that easily:

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

And that's the thing.

Of some further applications, as guys already said in the comments, transformation to CPS moves every application to tail-position, so the call stack is being replaced with lambdas, and further if you defunctionalize them, what you get is a data structure representing the control flow -- a neat form to be converted to, say C, or some other imperative language. Fully automagicaly! Or, if you'd like to implement some monad mumbo-jumbo, say Maybe monad, in CPS it's easy, just prepend to each continuation-lambda the test on whether the received value is "Just something" (in which case do the job and push the result to your continuation), or "Nothing", in which case you just push Nothing (to the continuation-lambda). Of course rather by another program or macro, not by hand, as it might be tedious -- the most magic behing cps is that it's so easy to automate the transformation to cps.

Hope I didn't make it unnecessarily complicated.

查看更多
登录 后发表回答