2

The parameter of call/cc is a procedure taking as its argument a continuation. Is the procedure written in CPS?

2 Answers2

5

No.

CPS-styled functions expect other normal functions as their argument(s), and may call them in tail position. These functions are confusingly called "continuations" in Scheme vernacular. I prefer "contingencies", to disambiguate.

The argument function to call/cc expects an actual undelimited continuation as its argument. That actual continuation is not a function. Calling it with a value returns that value into that continuation's return context which is thus saved along with the continuation -- a feat unheard of w.r.t. simple functions.

A tail-called function returns its result into its calling function's caller's context.

A continuation which is called returns the supplied value to its creating call/cc call's context. It is thus not a function. Thus a function using it is not written in CPS.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • The jargon du jour for functions that are passed as arguments seems to be "callbacks". – Barmar Aug 09 '19 at 23:24
  • I think we're interpreting the question differently. I don't think he's asking whether the continuation is written in CPS. – Barmar Aug 09 '19 at 23:25
  • @Barmar aah, callbacks could've been clearer. I came up with that word usage originally in the context of having two "continuations", success and failure. *on-success* and *on-failure* would be even more fitting to the "callbacks" POV. OTOH *callbacks* per se are more loose, because conceivably they can return to their caller, and/or used for effects. the "contingencies" never do, as you know they are supplied with other contingencies to call instead. that's another thing that makes the argument to call/cc to *not* be a contingency-styled i.e. CPS function. :) – Will Ness Aug 10 '19 at 15:09
  • @Barmar "I don't think he's asking whether the continuation is written in CPS." that's not what I'm saying either. the argument to call/cc is not a continuation. it is a regular function. the OP asks whether it is written in CPS style. My answer is no, it isn't; and it *can't be* either, because it can't take a contingency function as an argument, as required of the CPS-styled functions. Its only argument is an undelimited continuation supplied by the system. – Will Ness Aug 10 '19 at 15:42
1

Firstly, what is CPS? Continuation-Passing Style is a way of compiling a program which relies on continuations (i.e. uses the call/cc operator) into a target language which doesn't support continuation, but does support lexical closures and tail calls (or some facsimile of tail calls, like the ability to roll back the stack when it gets too deep with actual call frames).

A program transformed with CPS is itself not written in the continuation-passing style. It doesn't have to be; it has a call/cc operator provided by the CPS translator, which gives it access to the current continuation wherever it is needed.

Because CPS is mostly a source-to-source transformation, it is illustrated and taught using hand-written, explicit CPS. Quite often, the Scheme language is used for this. The Scheme language already has call/cc, but when experimenting with explicit, hand-written CPS, we have to pretend that call/cc doesn't exist. Under the CPS paradigm, we can, of course, provide an operator called my-call/cc which is built on our CPS, and has nothing to do with the underlying Scheme's call/cc.

In a CPS-compiled language implementation, every procedure has a continuation argument (with the necessary exception of procedures in the host language's library). The functional argument of call/cc, the function which receives the continuation, is no exception. Being a procedure in a CPS-world, it has to have continuation parameter, so that it is compatible with procedure calls, which pass that argument.

The argument procedure of call/cc can in fact use that continuation argument, and the very first example of the Wikipedia demonstrates this. Here is a copy of that example, in which I renamed the return parameter to c, to reduce confusion:

(define (f c)       ;; function used for capturing continuation
  (c 2)             ;; invoke continuation
  3)                ;; if continuation returns, return 3.

(display (f (lambda (x) x))) ; displays 3

(display (call/cc f)) ; displays 2

The procedure f's c argument doesn't have to be a continuation; in the first call it's just the dummy function (lambda (x) x). f calls it, and that function returns, and so then control falls through to the 3.

Under CPS, f has a hidden argument, which we can reveal with hand-written CPS:

(define (f c k)
  (c 2 k)
  (k 3 k))

When 3 is being returned, it's because the hidden continuation is invoked. Since the program can do that, f has to have one.

Note that c and k are different continuations! Maybe that's where the confusion comes in. The c continuation is the caller's current one that is passed by call/cc, and is part of the explicit semantics of call/cc. The k is the hidden one added by the CPS transformer. It's f's own continuation. Every function has one in the CPS-transformed world. Under the CPS paradigm, if f wishes to return, it calls k (which is why I renamed return to c). If f wishes to continue the suspended call/cc, it calls c.

By the way, under automatic CPS, k is not going to be literally called k, because that wouldn't be hygienic: programs can freely bind the k symbol. It will have to be a machine-generated symbol (gensym), or receive some other form of hygienic treatment.

The only function that have to be treated specially under CPS so that they don't have continuation arguments are library functions in the host language/VM that are to be available to the CPS-translated language. When the CPS-translated language calls (display obj) to print an object, that display function either has to be a renamed wrapper that can take the continuation argument (and then ignore it and call the real display function without it), or else the call to display has to be specially handled by the CPS translator to omit the continuation argument.

Lastly, why can CPS implement continuations in a language/VM that doesn't natively provide them? The reason is that all function calls in CPS-transformed programs are tail calls, and so never return. The tricky part in implementing continuations is capturing the entire call stack so that when a continuation is resumed, it can return. Such a feature can only be added at the language implementation level. In the 1970's, InterLisp used "spaghetti stacks" to implement this: stack frames are garbage collected heap objects, pointing to parent frames. But what if functions don't do such a thing as returning? Then the need to add a spaghetti stack to the implementation goes away. Note that the spaghetti stack has not exactly gone away: we have something equivalent under CPS, namely chains of captured lexical environments. A continuation is a lambda, which captures the surrounding procedure's k parameter, which is itself a lambda that has captured its parent's k parameter, ... and so on: it's a chain of environments, similar to stack frames, in which the hidden k parameters are frame pointers. But the host language already gave us lexically capturing lambdas; we have just leveraged these lambdas to de facto represent the continuation spaghetti stack, and so we didn't have to go down to the implementation level to do anything.

Kaz
  • 55,781
  • 9
  • 100
  • 149
  • I'm having doubts about your CPS translation of `f`, but haven't had the chance to work it out myself yet. :) I'm quite sure it's not right -- no CPS function body can have more than one form in it, right? – Will Ness Aug 22 '19 at 23:38
  • @WillNess RIght. Basically, under CPS, we can't expect `(c 2 k)` to return and fall through to to the next statement; we have to encode that future into the transformation, wrapped as a lambda continuation: `(c 2 (lambda (ignored kk) (kk 3 k)))` or something like that. – Kaz Aug 23 '19 at 00:09