PL: Lecture #28  Tuesday, April 20th
(text)

More Web Transformations

PLAI §17

Transforming a recursive function

Going back to transforming code, we did the transformation on a simple expression — and as you’d guess, it’s possible to make it work for more complex code, even recursive functions. Let’s start with some simple function that sums up a bunch of numbers, given a list of prompts for these numbers. Since it’s a function, it’s a reusable piece of code that can be used in multiple places, and to demonstrate that, we add a web-display with a specific list of prompts.

(define (sum prompts)
  (if (null? prompts)
    0
    (+ (web-read (first prompts))
      (sum (rest prompts)))))

(web-display (sum '("First" "Second" "Third")))

We begin by converting the web-read to its continuation version:

(define (sum prompts)
  (if (null? prompts)
    0
    (web-read/k (first prompts)
      (lambda (n)
        (+ n
          (sum (rest prompts)))))))

(web-display (sum '("First" "Second" "Third")))

But using web-read/k immediately terminates the running computation, which means that when sum is called on the last line, the surrounding web-display will be lost, and therefore this will not work. The way to solve this is to make sum itself take a continuation, which we’ll get in a similar way — by rewriting it as a sum/k function, and then we can make our sample use pull in the web-display into the callback as we’ve done before:

(define (sum/k prompts k)
  (if (null? prompts)
    0
    (web-read/k (first prompts)
      (lambda (n)
        (+ n
          (sum (rest prompts)))))))

(sum/k '("First" "Second" "Third")
      (lambda (sum) (web-display sum)))

We also need to deal with the recursive sum call and change it to a sum/k. Clearly, the continuation is the same continuation that the original sum was called with, so we need to pass it on in the recursive call too:

(define (sum/k prompts k)
  (if (null? prompts)
    0
    (web-read/k (first prompts)
      ;; get the value provided by the user, and add it to the value
      ;; that the recursive call generates
      (lambda (n)
        (+ n
          (sum/k (rest prompts)
                  k))))))

(sum/k '("First" "Second" "Third")
      (lambda (sum) (web-display sum)))

But there is another problem now: the addition is done outside of the continuation, therefore it will be lost as soon as there’s a second web-read/k call. In other words, computation bits that are outside of any continuations are going to disappear, and therefore they must be encoded as an explicit part of the continuation. The solution is therefore to move the addition into the continuation:

(define (sum/k prompts k)
  (if (null? prompts)
    0
    (web-read/k (first prompts)
      (lambda (n)
        (sum/k (rest prompts)
              (lambda (sum-of-rest)
                (k (+ n sum-of-rest))))))))

(sum/k '("First" "Second" "Third")
      (lambda (sum) (web-display sum)))

Note that with this code every new continuation is bigger — it contains the previous continuation (note that “contains” here is done by making it part of the closure), and it also contains one new addition.

But if the continuation is only getting bigger, then how do we ever get a result out of this? Put differently, when we reach the end of the prompt list, what do we do? — Clearly, we just return 0, but that silently drops the continuation that we worked so hard to accumulate. This means that just returning 0 is wrong — instead, we should send the 0 to the pending continuation:

(define (sum/k prompts k)
  (if (null? prompts)
    (k 0)
    (web-read/k (first prompts)
      (lambda (n)
        (sum/k (rest prompts)
              (lambda (sum-of-rest)
                (k (+ n sum-of-rest))))))))

(sum/k '("First" "Second" "Third")
      (lambda (sum) (web-display sum)))

This makes sense now, and the code works as expected. This sum/k is a utility to be used in a web server application, and such applications need to be transformed in a similar way to what we’re doing. Therefore, our own sum/k is a function that expects to be invoked from such transformed code — so it needs to have an argument for the waiting receiver, and it needs to pass that receiver around (accumulating more functionality into it) until it’s done.

As a side note, web-display is the only thing that is used in the toplevel continuation, so we could have used it directly without a lambda wrapper:

(sum/k '("First" "Second" "Third")
      web-display)

Using sum/k

To get some more experience with this transformation, we’ll try to convert some code that uses the above sum/k. For example, lets add a multiplier argument that will get multiplied by the sum of the given numbers. Begin with the simple code. This is an actual application, so we’re writing just an expression to do the computation and show the result, not a function.

(web-display (* (web-read "Multiplier")
                (sum '("First" "Second" "Third"))))

We now need to turn the two function calls into their */k form. Since we covered sum/k just now, begin with that. The first step is to inspect its continuation: this is the same code after we replace the sum call with a hole:

(web-display (* (web-read "Multiplier")
                <*>))

Now take this expression, make it into a function by abstracting over the hole and call it n, and pass that to sum/k:

(sum/k '("First" "Second" "Third")
      (lambda (n)
        (web-display (* (web-read "Multiplier")
                        n))))

(Note that this is getting rather mechanical now.) Now for the web-read part, we need to identify its continuation — that’s the expression that surrounds it inside the first continuation function, and we’ll use m for the new hole:

        (* m
            n)

As above, abstract over m to get a continuation, and pass it into web-read/k:

(sum/k '("First" "Second" "Third")
      (lambda (n)
        (web-read/k "Multiplier"
                    (lambda (m)
                      (web-display (* m n))))))

and we’re done. An interesting question here is what would happen if instead of the above, we start with the web-read and then get to the sum? We’d end up with a different version:

(web-read/k "Multiplier"
            (lambda (m)
              (sum/k '("First" "Second" "Third")
                    (lambda (n)
                      (web-display (* m n))))))

Note how these options differ — one reads the multiplier first, and the other reads it last.

Side-note: if in the last step of turning web-read to web-read/k we consider the whole expression when we formulate the continuation, then we get to the same code. But this isn’t really right, since it is converting code that is already-converted.

In other words, our conversion results in code that fixes a specific evaluation order for the original expression. The way that the inputs happen in the original expression

(web-display (* (web-read "Multiplier")
                (sum '("First" "Second" "Third"))))

is unspecified in the code — it only happens to be left-to-right implicitly, because Racket evaluates function arguments in that order. However, the converted code does not depend on how Racket evaluates function arguments. (Can you see a similar conclusion here about strictness?)

Note also another property of the converted code: every intermediate result has a name now. This makes sense, since another way to fix the evaluation order is to do just that. For example, convert the above to either

(let* ([m (web-read "Multiplier")]
      [n (sum '("First" "Second" "Third"))])
  (* m n))

or

(let* ([n (sum '("First" "Second" "Third"))]
      [m (web-read "Multiplier")])
  (* m n))

This is also a good way to see why this kind of conversion can be a useful tool in compiling code: the resulting code is in a kind of a low-level form that makes it easy to translate to assembly form, where function calls are eliminated, and instead there are only jumps (since all calls are tail-calls). In other words, the above can be seen as a piece of code that is close to something like:

val n = sum(["First","Second","Third"])
val m = web_read("Multiplier")
web_display(m*n)

and it’s almost visible in the original converted code if we format it in a very weird way:

;; sum(["First","Second","Third"]) -> n
(sum/k '("First" "Second" "Third") (lambda (n)
;; web_read("Multiplier") -> m
(web-read/k "Multiplier" (lambda (m)
;; web_display(m*n)
(web-display (* m n))))))

Converting stateful code

Another case to consider is applying this transformation to code that uses mutation with some state. For example, here’s some simple account code that keeps track of a balance state:

(define account
  (let ([balance (box 0)])
    (lambda ()
      (set-box! balance
                (+ (unbox balance)
                  (web-read (format "Balance: ~s; Change"
                                    (unbox balance)))))
      (account))))

(Note that there is no web-display here, since it’s an infinite loop.) As usual, the fact that this function is expected to be used by a web application means that it should receive a continuation:

(define account/k
  (let ([balance (box 0)])
    (lambda (k)
      (set-box! balance
                (+ (unbox balance)
                  (web-read (format "Balance: ~s; Change"
                                    (unbox balance)))))
      (account))))

Again, we need to convert the web-read into web-read/k by abstracting out its continuation. We’ll take the set-box! expression and create a continuation out of it:

      (set-box! balance
                (+ (unbox balance)
                  <*>))

and using change as the name for the continuation argument, we get:

(define account/k
  (let ([balance (box 0)])
    (lambda (k)
      (web-read/k (format "Balance: ~s; Change"
                          (unbox balance))
                  (lambda (change)
                    (set-box! balance (+ (unbox balance) change))))
      (account))))

And finally, we translate the loop call to pass along the same continuation it received (it seems suspicious, but there’s nothing else that could be used there):

(define account/k
  (let ([balance (box 0)])
    (lambda (k)
      (web-read/k (format "Balance: ~s; Change" (unbox balance))
                  (lambda (change)
                    (set-box! balance (+ (unbox balance) change))))
      (account/k k))))

But if we try to run this — (account/k web-display) — we don’t get any result at all: it reads one number and then just stops without the usual request to continue, and without showing any result. The lack of printed result is a hint for the problem — it must be the void return value of the set-box!. Again, we need to remember that invoking a web-read/k kills any pending computation and the following (resume) will restart its continuation — but the recursive call is not part of the loop.

The problem is the continuation that we formulated:

      (set-box! balance
                (+ (unbox balance)
                  change))

which should actually contain the recursive call too:

      (set-box! balance
                (+ (unbox balance)
                  change))
      (account/k k)

In other words, the recursive call was left outside of the continuation, and therefore it was lost when the fake server terminated the computation on a web-read/k — so it must move into the continuation as well:

(define account/k
  (let ([balance (box 0)])
    (lambda (k)
      (web-read/k (format "Balance: ~s; Change" (unbox balance))
                  (lambda (change)
                    (set-box! balance (+ (unbox balance) change))
                    (account/k k))))))

and the code now works. The only suspicious thing that we’re still left with is the loop that passes k unchanged — but this actually is the right thing to do here. The original loop had a tail-recursive call that didn’t pass along any new argument values, since the infinite loop is doing its job via mutations to the box and nothing else was done in the looping call. The continuation of the original call is therefore also the continuation of the second call, etc. All of these continuations are closing over a single box and this binding does not change (it cannot change if we don’t use a set!); instead, the boxed value is what changes through the loop.

Converting higher order functions

Next we try an even more challenging transformation: a higher order function. To get a chance to see more interesting examples, we’ll have some more code in this case.

For example, say that we want to compute the sum of squares of a list. First, the simple code (as above, there’s no need to wrap a web-display around the whole thing, just make it return the result):

(define (sum l) (foldl + 0 l))
(define (square n) (* n n))
(define (read-number prompt)
  (web-read (format "~a number" prompt)))
(web-display (sum (map (lambda (prompt)
                        (square (read-number prompt)))
                      '("First" "Second" "Third"))))

Again, we can begin with web-read — we want to convert it to the continuation version, which means that we need to convert read-number to get one too. This transformation is refreshingly trivial:

(define (read-number/k prompt k)
  (web-read/k (format "~a number" prompt) k))

This is an interesting point — it’s a simple definition that just passes k on, as is. The reason for this is similar to the simple continuation passing of the imperative loop: the pre-translation read-number is doing a simple tail call to web-read, so the evaluation context of the two is identical. The only difference is the prompt argument, and that’s the same format call.

Of course things would be different if format itself required a web interaction, since then we’d need some format/k, but without that things are really simple. The same goes for the two utility functions — sum and square: they’re not performing any web interaction so it seems likely that they’ll stay the same.

We now get to the main expression, which should obviously change since it needs to call read-number/k, so it needs to send it some continuation. By now, it should be clear that passing an identity function as a continuation is going to break the surrounding context once the running computation is killed for the web interaction. We need to somehow generate a top-level identity continuation and propagate it inside, and the sum call should be in that continuation together with the web-display call. Actually, if we do the usual thing and write the expression with a <*> hole, we get:

(web-display (sum (map (lambda (prompt) (square <*>))
                      '("First" "Second" "Third"))))

and continuing with the mechanical transformation that we know, we need to abstract over this expression+hole into a function, then pass it as an argument to read-number/k:

;; very broken
(read-number/k
(lambda (<*>)
  (web-display (sum (map (lambda (prompt) (square <*>))
                          '("First" "Second" "Third"))))))

But that can’t work in this case — we need to send read-number/k a prompt, but we can’t get a specific one since there is a list of them. In fact, this is related to a more serious problem — pulling out read-number/k like this is obviously broken since it means that it gets called only once, instead, we need to call it once for each prompt value.

The solution in this case is to convert map too:

(web-display (sum (map/k (lambda (prompt)
                          (square (read-number prompt)))
                        '("First" "Second" "Third")
                        ...some-continuation...)))

and of course we should move web-display and sum into that continuation:

(map/k (lambda (prompt) (square (read-number prompt)))
      '("First" "Second" "Third")
      (lambda (l) (web-display (sum l))))

We can now use read-number/k, but the question is what should it get for it’s continuation?

(map/k (lambda (prompt) (square (read-number/k prompt ???)))
      '("First" "Second" "Third")
      (lambda (l) (web-display (sum l))))

Clearly, map/k will need to somehow communicate some continuation to the mapped function, which in turn will send it to read-number/k. This means that the mapped function should get converted too, and gain a k argument. To do this, we’ll first make things convenient and have a name for it (this is only for convenience, we could just as well convert the lambda directly):

(define (read-squared prompt)
  (square (read-number/k prompt ???)))
(map/k read-squared
      '("First" "Second" "Third")
      (lambda (l) (web-display (sum l))))

Then convert it in the now-obvious way:

(define (read-squared/k prompt k)
  (read-number/k prompt
                (lambda (n)
                  (k (square n)))))
(map/k read-squared/k
      '("First" "Second" "Third")
      (lambda (l) (web-display (sum l))))

Everything is in place now — except for map/k, of course. We’ll start with the definition of plain map:

(define (map f l)
  (if (null? l)
    null
    (cons (f (first l)) (map f (rest l)))))

The first thing in turning it into a map/k is adding a k input,

(define (map f l k)
  (if (null? l)
    null
    (cons (f (first l)) (map f (rest l)))))

and now we need to face the fact that the f input is itself one with a continuation — an f/k:

(define (map/k f/k l k)
  (if (null? l)
    null
    (cons (f (first l)) (map f (rest l)))))

Consider now the single f call — that should turn into a call to f/k with some continuation:

(define (map/k f/k l k)
  (if (null? l)
    null
    (cons (f/k (first l) ???) (map f (rest l)))))

but since f/k will involve a web interaction, it will lead to killing the cons around it. The solution is to move that cons into the continuation that is handed to f/k — and as usual, this involves the second cons argument — the continuation is derived from replacing the f/k call by a hole:

    (cons <*> (map f (rest l)))

and abstracting that hole, we get:

(define (map/k f/k l k)
  (if (null? l)
    null
    (f/k (first l)
        (lambda (result)
          (cons result (map f (rest l)))))))

We now do exactly the same for the recursive map call — it should use map/k with f/k and some continuation:

(define (map/k f/k l k)
  (if (null? l)
    null
    (f/k (first l)
        (lambda (result)
          (cons result (map/k f/k (rest l) ???))))))

and we need to move the surrounding cons yet again into this continuation. The holed expression is:

          (cons result <*>)

and abstracting that and moving it into the map/k continuation we get:

(define (map/k f/k l k)
  (if (null? l)
    null
    (f/k (first l)
        (lambda (result)
          (map/k f/k (rest l)
                  (lambda (new-rest)
                    (cons result new-rest)))))))

There are just one more problem with this — the k argument is never used. This implies two changes, since it needs to be used once in each of the conditional branches. Can you see where it should be added? (Try to do this before reading the code below.)

The complete code follows:

(define (map/k f/k l k)
  (if (null? l)
    (k null)
    (f/k (first l)
        (lambda (result)
          (map/k f/k (rest l)
                  (lambda (new-rest)
                    (k (cons result new-rest))))))))
(define (sum l) (foldl + 0 l))
(define (square n) (* n n))
(define (read-number/k prompt k)
  (web-read/k (format "~a number" prompt) k))
(define (read-squared/k prompt k)
  (read-number/k prompt (lambda (n) (k (square n)))))
(map/k read-squared/k
      '("First" "Second" "Third")
      (lambda (l) (web-display (sum l))))

Highlevel Overview on Continuations

Very roughly speaking, the transformation we made turns a function call like

(...stuff... (f ...args...) ...more-stuff...)

into

(f/k ...args...
    (lambda (<*>)
      (...stuff... <*> ...more-stuff...)))

This is the essence of the solution to the statelessness problem: to remember where we left off, we conveniently flip the expression inside-out so that its context is all stored in its continuation. One thing to note is that we did this only for functions that had some kind of web interaction, either directly or indirectly (since in the indirect case they still need to carry around the continuation).

If we wanted to make this process a completely mechanical one, then we wouldn’t have been able to make this distinction. After all, a function like map is perfectly fine as it is, unless it happens to be used with a continuation-carrying function — and that’s something that we know only at runtime. We would therefore need to transform all function calls as above, which in turn means that all functions would need to get an extra continuation argument.

Here are a few things to note about such fully-transformed code:

What we did here is the tedious way of getting continuations: we basically implemented them by massaging our code, turning it inside-out into code with the right shape. The problem with this is that the resulting code is no longer similar to what we had originally written, which makes it more difficult to debug and to maintain. We therefore would like to have this done in some automatic way, ideally in a way that means that we can leave our plain original code as is.

An Automatic Continuation Converter

PLAI §18

The converted code that we produced manually above is said to be written in “Continuation Passing Style”, or CPS. What we’re looking for is for a way to generate such code automatically — a way to “CPS” a given source code. When you think about it, this process is essentially a source to source function which should be bolted onto the compiler or evaluator. In fact, if we want to do this in Racket, then this description makes it sound a lot like a macro — and indeed it could be implemented as such.

Note that “CPS” has two related but distinct meanings here: you could have code that is written “in CPS style”, which means that it handles explicit continuations. Uses of this term usually refer to using continuation functions in some places in the code, not for fully transformed code. The other meaning is used for fully-CPS-ed code, which is almost never written directly. In addition, “CPS” is often used as a verb — either the manual process of refactoring code into passing some continuations explicitly (in the first case), or the automatic process of fully converting code (in the second one).

Before we get to the actual implementation, consider how we did the translation — there was a difference in how we handled plain top-level expressions and library functions. In addition, we had some more discounts in the manual process — one such discount was that we didn’t treat all value expressions as possible computations that require conversion. For example, in a function application, we took the function sub-expression as a simple value and left it as is, but for an automatic translation we need to convert that expression too since it might itself be a more complicated expression.

Instead of these special cases and shortcuts, we’ll do something more uniform: we will translate every expression into a function. This function will accept a receiver (= a continuation) and will pass it the value of the expression. This will be done for all expressions, even simple ones like plain numbers, for example, we will translate the 5 expression into (lambda (k) (k 5)), and the same goes for other constants and plain identifiers. Since we’re specifying a transformation here, we will treat it as a kind of a meta function and use a CPS[x] to make it easy to talk about:

CPS[5]
-->
(lambda (k) (k 5)) ; same for other numbers and constants

CPS[x]
-->
(lambda (k) (k x)) ; same as above for identifiers too

When we convert a primitive function application, we still do the usual thing, which is now easier to see as a general rule — using CPS[?] as the meta function that does the transformation:

CPS[(+ E1 E2)]
-->
(lambda (k)        ; everything turns to cont.-consuming functions
  (CPS[E1]        ; the CPS of E1 -- it expects a cont. argument
  (lambda (v1)    ; send this cont. to CPS[E1], so v1 is its value
    (CPS[E2]      ; same for E2 -- expects a cont.
      (lambda (v2) ; and again, v2 becomes the value of E2
        (k (+ v1 v2))))))) ; finally return the sum to our own cont.

In the above, you can see that (CPS[E] (lambda (v) …)) can be read as “evaluate E and bind the result to v”. (But note that the CPS conversion is not doing any evaluation, it just reorders code to determine how it gets evaluated when it later runs — so “compute” might be a better term to use here.) With this in mind, we can deal with other function applications: evaluate the function form, evaluate the argument form, then apply the first value on the second value, and finally wrap everything with a (lambda (k) …) and return the result to this continuation:

CPS[(E1 E2)]
-->
(lambda (k)
  (CPS[E1]        ; bind the result of evaluating E1
  (lambda (v1)    ; to v1
    (CPS[E2]      ; and the result of evaluating E2
      (lambda (v2) ; to v2
        (k (v1 v2))))))) ; apply and return the result

But this is the rule that we should use for primitive non-continuation functions only — it’s similar to what we did with + (except that we skipped evaluating + since it’s known). Instead, we’re dealing here with functions that are defined in the “web language” (in the code that is being converted), and as we’ve seen, these functions get a k argument which they use to return the result to. That was the whole point: pass k on to functions, and have them return the value directly to the k context. So the last part of the above should be fixed:

CPS[(E1 E2)]
-->
(lambda (k)
  (CPS[E1]        ; bind the result of evaluating E1
  (lambda (v1)    ; to v1
    (CPS[E2]      ; and the result of evaluating E2
      (lambda (v2) ; to v2
        (v1 v2 k)))))) ; apply and have it return the result to k

There’s a flip side to this transformation — whenever a function is created with a lambda form, we need to add a k argument to it, and make it return its value to it. Then, we need to “lift” the whole function as usual, using the same transformation we used for other values in the above. We’ll use k for the latter continuation argument, and cont for the former:

CPS[(lambda (arg) E)]
-->
(lambda (k) ; this is the usual
  (k        ; lifting of values
  (lambda (arg cont) ; the translated function has a cont. input
    (CPS[E] cont)))) ; the translated body returns its result to it

It is interesting to note the two continuations in the translated result: the first one (using k) is the continuation for the function value, and the second one (using cont) is the continuation used when the function is applied. Comparing this to our evaluators — we can say that the first roughly corresponds to evaluating a function form to get a closure, and the second corresponds to evaluating the body of a function when it’s applied, which means that cont is the dynamic continuation that matches the dynamic context in which the function is executed. Inspecting the CPS-ed form of the identity function is unsurprising: it simply passes its first argument (the “real” one) into the continuation since that’s how we return values in this system:

CPS[(lambda (x) x)]
-->
(lambda (k)
  (k
  (lambda (x cont)
    (CPS[x] cont))))
-->
(lambda (k)
  (k
  (lambda (x cont)
    ((lambda (k) (k x)) cont))))
--> ; reduce the redundant function application
(lambda (k)
  (k
  (lambda (x cont)
    (cont x))))

Note the reduction of a trivial application — doing this systematic conversion leads to many of them.

We now get to the transformation of the form that is the main reason we started with all of this — web-read. This transformation is simple, it just passes along the continuation to web-read/k:

CPS[(web-read E)]
-->
(lambda (k)
  (CPS[E]          ; evaluate the prompt expression
  (lambda (prompt) ; and bind it to prompt
    (web-read/k prompt k)))) ; use the prompt and the current cont.

We also need to deal with web-display — we changed the function calling protocol by adding a continuation argument, but web-display is defined outside of the CPS-ed language so it doesn’t have that argument. Another way of fixing it could be to move its definition into the language, but then we’ll still need to have a special treatment for the error that it uses.

CPS[(web-display E)]
-->
(lambda (k)
  (CPS[E]          ; evaluate the expression
  (lambda (val)  ; and bind it to val
    (web-display val))))

As you can see, all of these transformations are simple rewrites. We can use a simple syntax-rules macro to implement this transformation, essentially creating a DSL by translating code into plain Racket. Note that in the specification above we’ve implicitly used some parts of the input as keywords — lambda, +, web-read, and define — this is reflected in the macro code. The order of the rules is important, for example, we need to match first on (web-read E) and then on the more generic (E1 E2), and we ensure that the last default lifting of values has a simple expression by matching on (x …) before that.

(define-syntax CPS
  (syntax-rules (+ lambda web-read web-display) ;*** keywords
    [(CPS (+ E1 E2))
    (lambda (k)
      ((CPS E1)
        (lambda (v1)
          ((CPS E2)
          (lambda (v2)
            (k (+ v1 v2)))))))]
    [(CPS (web-read E))
    (lambda (k)
      ((CPS E)
        (lambda (val)
          (web-read/k val k))))]
    [(CPS (web-display E))
    (lambda (k)                ; could be:
      ((CPS E)                ;    (lambda (k)
        (lambda (val)          ;      ((CPS E) web-display))
          (web-display val))))] ; but keep it looking uniform
    [(CPS (lambda (arg) E))
    (lambda (k)
      (k (lambda (arg cont)
            ((CPS E)
            cont))))]
    [(CPS (E1 E2))
    (lambda (k)
      ((CPS E1)
        (lambda (v1)
          ((CPS E2)
          (lambda (v2)
            (v1 v2 k))))))]
    ;; the following pattern ensures that the last rule is used only
    ;; with simple values and identifiers
    [(CPS (x ...))
    ---syntax-error---]
    [(CPS V)
    (lambda (k) (k V))]))

The transformation that this code implements is one of the oldest CPS transformations — it is called the Fischer Call by Value CPS transformation, and is due Michael Fischer. There has been much more research into such transformations — the Fischer translation, while easy to understand due to its uniformity, introduces significant overhead in the form of many new functions in its result. Some of these are easy to optimize — for example, things like ((lambda (k) (k v)) E) could be optimized to just (E v) assuming a left-to-right evaluation order or proving that E has no side-effects (and Racket performs this optimization and several others), but some of the overhead is not easily optimized. There have been several other CPS transformations, in an attempt to avoid such overhead.

Finally, trying to run code using this macro can be a little awkward. We need to explicitly wrap all values in definitions by a CPS, and we need to invoke top-level expressions with a particular continuation — web-display in our context. We can do all of that with a convenience macro that will transform a number of definitions followed by an optional expression.

Note the use of begin — usually, it is intended for sequential execution, but it is also used in macro result expressions when we need a macro to produce multiple expressions (since the result of a macro must be a single S-expression) — this is why it’s used here, not for sequencing side-effects.

(define-syntax CPS-code
  (syntax-rules (define)
    [(CPS-code (define (id arg) E) more ...)
    ;; simple translation to `lambda'
    (CPS-code (define id (lambda (arg) E)) more ...)]
    [(CPS-code (define id E) more ...)
    (begin (define id ((CPS E) (lambda (x) x)))
            (CPS-code more ...))]
    [(CPS-code last-expr)
    ((CPS last-expr) web-display)]
    [(CPS-code) ; happens when there is no plain expr at
    (begin)])) ; the end so do nothing in this case

The interesting thing that this macro does is set up a proper continuation for definitions and top-level expressions. In the latter case, it passes web-display as the continuation, and in the former case, it passes the identity function as the continuation — which is used to “lower” the lifted value from its continuation form into a plain value. Using the identity function as a continuation is not really correct: it means that if evaluating the expression to be bound performs some web interaction, then the definition will be aborted, leaving the identifier unbound. The way to solve this is by arranging for the definition operation to be done in the continuation, for example, we can get closer to this using an explicit mutation step:

    [(CPS-code (define id E) more ...)
    (begin (define id #f)
            ((CPS E) (lambda (x) (set! id x)))
            (CPS-code more ...))]

But there are two main problems with this: first, the rest of the code — (CPS-code more ...) — should also be done in the continuation, which will defeat the global definitions. We could try to use the continuation to get the scope:

    [(CPS-code (define id E) more ...)
    ((CPS E) (lambda (id) (CPS-code more ...)))]

but that breaks recursive definitions. In any case, the second problem is that this is not accurate even if we solved the above: we really need to have parts of the Racket definition mechanism exposed to make it work. So we’ll settle with the simple version as an approximation. It works fine if we use definitions only for functions, and invoke them in toplevel expressions.

For reference, the complete code at this point follows.

;; Simulation of web interactions with a CPS converter (not an
;; interpreter)

#lang racket

(define error raise-user-error)

(define (nothing-to-do ignored)
  (error 'nothing-to-do "No computation to resume."))

(define resumer (box nothing-to-do))

(define (web-display n)
  (set-box! resumer nothing-to-do)
  (error 'web-display "~s" n))

(define (web-read/k prompt k)
  (set-box! resumer k)
  (error 'web-read
        "enter (submit N) to continue the following\n  ~a:"
        prompt))

(define (submit n)
  ;; to avoid mistakes, we clear out `resumer' before invoking it
  (let ([k (unbox resumer)])
    (set-box! resumer nothing-to-do)
    (k n)))

(define-syntax CPS
  (syntax-rules (+ lambda web-read web-display) ;*** keywords
    [(CPS (+ E1 E2))
    (lambda (k)
      ((CPS E1)
        (lambda (v1)
          ((CPS E2)
          (lambda (v2)
            (k (+ v1 v2)))))))]
    [(CPS (web-read E))
    (lambda (k)
      ((CPS E)
        (lambda (val)
          (web-read/k val k))))]
    [(CPS (web-display E))
    (lambda (k)
      ((CPS E)
        (lambda (val)
          (web-display val))))]
    [(CPS (lambda (arg) E))
    (lambda (k)
      (k (lambda (arg cont)
            ((CPS E)
            cont))))]
    [(CPS (E1 E2))
    (lambda (k)
      ((CPS E1)
        (lambda (v1)
          ((CPS E2)
          (lambda (v2)
            (v1 v2 k))))))]
    ;; the following pattern ensures that the last rule is used only
    ;; with simple values and identifiers
    [(CPS (x ...))
    ---syntax-error---]
    [(CPS V) ; <-- only numbers, other literals, and identifiers
    (lambda (k)
      (k V))]))

(define-syntax CPS-code
  (syntax-rules (define)
    [(CPS-code (define (id arg) E) more ...)
    ;; simple translation to `lambda'
    (CPS-code (define id (lambda (arg) E)) more ...)]
    [(CPS-code (define id E) more ...)
    (begin (define id ((CPS E) (lambda (x) x)))
            (CPS-code more ...))]
    [(CPS-code last-expr)
    ((CPS last-expr) web-display)]
    [(CPS-code) ; happens when there is no plain expr at
    (begin)])) ; the end so do nothing in this case

Here is a quick example of using this:

(CPS-code
  (web-display (+ (web-read "First number")
                  (web-read "Second number"))))

Note that this code uses web-display, which is not really needed since CPS-code would use it as the top-level continuation. (Can you see why it works the same either way?) So this is even closer to a plain program:

(CPS-code (+ (web-read "First number")
            (web-read "Second number")))

A slightly more complicated example:

(CPS-code
  (define (add n)
    (lambda (m)
      (+ m n)))
  (define (read-and-add n)
    ((add n) (web-read "Another number")))
  (read-and-add (web-read "A number")))

Using this for the other examples is not possible with the current state of the translation macro. These example will require extending the CPS transformation with functions of any arity, multiple expressions in a body, and it recognize additional primitive functions. None of these is difficult, it will just make it more verbose.