2

https://en.wikipedia.org/wiki/Continuation-passing_style says

A function written in continuation-passing style takes an extra argument: an explicit "continuation", i.e. a function of one argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument. That means that when invoking a CPS function, the calling function is required to supply a procedure to be invoked with the subroutine's "return" value. Expressing code in this form makes a number of things explicit which are implicit in direct style. These include: procedure returns, which become apparent as calls to a continuation; intermediate values, which are all given names; order of argument evaluation, which is made explicit; and tail calls, which simply call a procedure with the same continuation, unmodified, that was passed to the caller.

How shall I understand that a function written in CPS "makes a number of things explicit", which include "procedure returns", "intermediate values", "order of argument evaluation", and "tail calls"?

For example, it seems to me that a function written in CPS makes the return value of the function implicit instead of explicit to the caller of the function.

For example in Haskell, a function not in CPS is

add :: Float -> Float -> Float
add a b = a + b

while it is written in CPS as:

add' :: Float -> Float -> (Float -> a) -> a
add' a b cont = cont (a + b)

Examples in Scheme are similar.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • `add' a b explicitReturn = explicitReturn (a + b)` makes it clear that `(a + b)` is explicitly returned. :) --- the thing about CPS is that it is assumed that no function ever returns in a "normal", i.e. usual sense. the "normal" return simply does not exist in CPS. if it did it'd guarantee stack overflow. all the tail calls are tail optimized and there are no non-tail calls. – Will Ness Aug 05 '19 at 16:18

2 Answers2

1

Imagine this expression:

3 * fun(a) + 4 * fun(b) + fun(c) * 5

We know the multiplications happen before the addition, but we don't know the order fun is called with a, b, and c. If you convert it to CPS the order becomes explicit. Eg.:

fun&(b, bv => 
    mul&(4, bv, bv4 => 
       fun&(c, cv => 
           mul&(cv, 5, cv5 => 
               fun&(a, av => 
                   mul&(3, av, av3 => 
                       sum&(av3, bv4, bv5, replPrint)))))));

It's obvious we have chosen to calculate fun(b) first since that is what is happening in the CPS version. In Common Lisp the order chosen is incorrect since it requires strict left to right. Thus in CL CPS isn't more explicit.

Haskell is lazy so evaluation order is even crazier since while you know in an eager language that in funa(funb(4)) funb is called and produced a value before calling funa this is not necessarily true for Haskell. funa might get called and funb(4) might not depending on the logic inside funa. Arguments get evaluated at need so if one isn't used it is never evaluated. While I haven't thought about it it should be possible to make CPS also for Haskell but it needs to be generated in a totally different way than I'm doing, which works for eager languages.

About the return

Notice that I have replPrint as the last continuation. The idea os that that is the consumer and the whole thing really never uses any return value. It's the value passed to continuation which is the returned value.

Micha Wiedenmann
  • 19,979
  • 21
  • 92
  • 137
Sylwester
  • 47,942
  • 4
  • 47
  • 79
  • 1
    `sum` would really be a pair of `add&` calls. The key point is that given a parse tree for that expression, there are a number of topological orderings of the tree which correspond to correct evaluations of the expression, and CPS makes explicit which ordering is used. – chepner Aug 05 '19 at 15:04
  • @chepner It depends on `+` being able to take more than 2 arguments or not. In Scheme `(+ a b c)` does not get split up in CPS but the underlying CPU operations would of course be 2 add instructions. – Sylwester Aug 05 '19 at 15:22
  • Given your original expression, then, it seems you silently rewrote `(+ (+ a b) c)` as `(+ a b c)`. – chepner Aug 05 '19 at 15:26
  • @chepner `a + b + c` = > `(+ a b c)` in Scheme. I agree I probably would have done `(+ (+ a b) c)` if I were to make the parser and the language was Algol based, but that is just laziness :p – Sylwester Aug 05 '19 at 15:41
  • Fair enough :) I don't know Scheme as well as I used to (which was never very well to begin with), and I was focusing more on Haskell since there was no Scheme code in the question itself. – chepner Aug 05 '19 at 15:44
  • @chepner Note that you don't need CPS to implement a language. You need it if you want to provide `call/cc`. Many modern Scheme implementations avoids CPS if they can. eg. Ikarus. It is not the same as AST since in Scheme the code is the AST. Imagine you do CPS, then lambda lifting (adding free variables as arguments to the continuation) then the expression in my answer wouldn't require any closures ad can be implemented with a while loop in a language without functions. – Sylwester Aug 05 '19 at 15:49
1

Using the expression in Sylwester's answer, the parser would generate an AST that looks something like this (with nodes numbered arbitrarily left to right, top to bottom, and the numbers noted in parentheses for later reference):

                   +
                  (1)
           /               \
          +                 *
         (2)               (3)
       /     \           /     \
      *       *        fun      5
     (4)     (5)       (6)     (7)
     / \     / \        |
    3 fun   4  fun      c
   (8) (9) (10) (11)    (12)
       |        |
       a        b
      (13)     (14)

(This assumes a left-associative + operator; one could also imagine equally valid trees for a right-associative +, or even a fully-associative operator yielding a tree with a single + node and three children.)

To evaluate an expression, you simply evaluate each node from the bottom up. Certain orderings are required by the tree itself: 13 has to come before 9; 4 and 5 must both be evaluated before 2, etc. However, for other pairs of nodes, the order is irrelevant. 6 can be evaluated either before or after 9, for example.

We can impose an ordering, though, by computing a topological sort of the tree, which is simply a list of nodes such that each child node always precede its parent in the ordering. There are multiple valid topological sorts for this tree, any of which will yield a correct value for the final expression. Some example orders are

  1. 13, 14, 12, 9, 11, 6, 8, 10, 7, 4, 5, 3, 2, 1

    This evaluates all function arguments first, then the function calls, then the multiplications, and finally the additions (all in left-to-right order)

  2. 8, 13, 9, 10, 14, 11, 12, 6, 7, 4, 5, 2, 3, 1

    This evaluates additive terms from left to right, so we complete a multiplication before evaluating the operands of the next multiplication.

  3. 8, 13, 9, 10, 11, 14, 4, 5, 2, 12, 7, 6, 3, 1

    This is like the second example, but also computes the first addition before tackling the operand for the second addition.


The punchline: CPS style simply makes explicit which ordering is used.

chepner
  • 497,756
  • 71
  • 530
  • 681