2013-04-12 - More Web Transformations - Highlevel Overview on Continuations - Implementing an Automatic Continuation Converter ======================================================================== >>> More Web Transformations [[[ PLAI Chapter 17 ]]] >> Transforming a recursive function We did the above transformation on a simple expression -- and as you'd guess, it's possible to make it work for recursive functions too, although it gets a little tricky. As done above, we start with some plain looking code: (define (sum prompts) (if (null? prompts) 0 (+ (web-read (first prompts)) (sum (rest prompts))))) Further, we want this function to be usable as a library function -- it should be useful for web applications that need this functionality. One result of this is that it doesn't `web-display' its result. 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))))))) But using `web-read/k' immediately terminates the running computation, so this won't work. Another way to see the problem is that the continuation input to `web-read/k' should have the rest of the computation, yet in this version it only has the addition -- if `sum' is used as in some larger context as a library function, then that whole context will be lost. 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: (define (sum/k prompts k) (if (null? prompts) 0 (web-read/k (first prompts) (lambda (n) (+ n (sum (rest prompts))))))) 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)))))) 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)))))))) 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)))))))) This makes sense now: 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. To try it, we need a top-level consumer value to pass on to the `sum/k' call. The obvious choice in this case is to use `web-display': (sum/k '("First" "Second" "Third") (lambda (sum) (web-display sum))) or, more simply: (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 converts already-converted code.) 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: n = sum(["First","Second","Third"]); m = web_read("Multiplier"); 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: * All function calls in such code are tail calls. There is no single call with some context around it that is left for the time when the call is done. This is the exact property that makes it useful for a stateless interaction: such contexts are bad since a web interaction will mean that the context is discarded and lost. (In our pretend system, this is done by throwing an error.) Having no non-tail context means that capturing the continuation argument is sufficient, and no context gets lost. * An implication of this, when you consider how the language is implemented, is that there is no need to have anything on "the stack" to execute fully transformed code. (If you'd use the stepper on such code, there would be no accumulation of context.) So is this some radical new way of computing without a stack? Not really: if you think about it, continuation arguments hold the exact same information that is traditionally put on the stack. (There is therefore an interesting relationship between continuations and runtime stacks, and in fact, one way of making it possible to grab continuations without doing such a transformation is to capture the current stack.) * The evaluation order is fixed. Obviously, if Racket guarantees a left-to-right evaluation, then the order is always fixed -- but in the fully transformed code there are no function calls where this makes any difference. If Racket were to change, the transformed code would still retain the order it uses. More specifically, when we do the transformation, we control the order of evaluation by choosing how to proceed at every point. But there's more: the resulting code is independent of the evaluation strategy of the language. Even if the language is lazy, the transformed code is still executing things in the same order. (Alternatively, we could convert things so that the resulting computation corresponds to a lazy evaluation strategy even in a strict language.) * In other words, the converted code is completely sequential. The conversion process requires choosing left-to-right or delaying some evaluations (or all), but the resulting code is free from any of these and has exactly one specific (sequential) order. You can therefore see how this kind of transformation is something that a compiler would want to do, since the resulting sequential code is easier for execution on a sequential base (like machine code, or C code). Another way to see this is that we have explicit names for each and every intermediate result -- so the converted code would have a direct mapping between identifiers and machine registers (unlike "plain" code where some of these are implicit and compilation needs to make up names). * The transformation is a *global* one. Not only do we have to transform the first top-level expression that makes up the web application, we also need to convert every function that is mentioned in the code, and in functions that those functions mentioned, etc. Even worse, the converted code is very different from the original version, since everything is shuffled around -- in a way that matches the sequential execution, but it's very hard to even see the original intention through all of these explicit continuations and the new intermediate result names. The upshot of this is that it's not really something that we need to do manually, but instead we'd like it to be done automatically for us, by the compiler of the language. 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. ======================================================================== >>> Implementing an Automatic Continuation Converter [[[ PLAI Chapter 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 for other identifiers 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) ; we 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)))) ; 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 (v) ; and bind it to v (web-read/k v k)))) ; use the prompt value 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 (v) ; and bind it to v (web-display v)))) 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 (v) (web-read/k v k))))] [(CPS (web-display E)) (lambda (k) ((CPS E) (lambda (v) (web-display v))))] [(CPS (E1 E2)) (lambda (k) ((CPS E1) (lambda (v1) ((CPS E2) (lambda (v2) (v1 v2 k))))))] [(CPS (lambda (arg) E)) (lambda (k) (k (lambda (arg cont) ((CPS E) cont))))] ;; 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 as macro results 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.] (define-syntax CPS-code (syntax-rules (define) [(CPS-code (define (id arg) E) more ...) (begin (define id ((CPS (lambda (arg) E)) (lambda (v) v))) (CPS-code more ...))] [(CPS-code (define id E) more ...) (begin (define id ((CPS E) (lambda (v) v))) (CPS-code more ...))] [(CPS-code last-expr) ((CPS last-expr) web-display)] [(CPS-code) ; happens when there is no non-definitions at the end (begin)])) ; so do nothing in this case The interesting thing that this macro does is arrange to have 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 (v) (set! id v))) (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 contiuation 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 this problem: we really need to have parts of the Racket definition mechanism exposed to make it work. So we settle with the above simple version as an approximation. For reference, the complete code at this point follows. ---<<>>------------------------------------------ ;; Simulation of web interactions with a CPS converter (not an ;; interpreter) #lang racket (define (nothing-to-do) (error "There is no computation in progress now.")) (define what-next (box nothing-to-do)) (define (web-display n) (set-box! what-next nothing-to-do) (error 'web-output "~s" n)) (define (web-read/k prompt k) (set-box! what-next (lambda () (printf "~a: " prompt) (k (read)))) (error 'web-read/k "enter (resume) to continue")) (define (resume) ;; to avoid mistakes, we clear out `what-next' before invoking it (let ([next (unbox what-next)]) (set-box! what-next nothing-to-do) (next))) (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 (v) (web-read/k v k))))] [(CPS (web-display E)) (lambda (k) ((CPS E) (lambda (v) (web-display v))))] [(CPS (E1 E2)) (lambda (k) ((CPS E1) (lambda (v1) ((CPS E2) (lambda (v2) (v1 v2 k))))))] [(CPS (lambda (arg) E)) (lambda (k) (k (lambda (arg cont) ((CPS E) cont))))] ;; 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))])) (define-syntax CPS-code (syntax-rules (define) [(CPS-code (define (id arg) E) more ...) (begin (define id ((CPS (lambda (arg) E)) (lambda (v) v))) (CPS-code more ...))] [(CPS-code (define id E) more ...) (begin (define id ((CPS E) (lambda (v) v))) (CPS-code more ...))] [(CPS-code last-expr) ((CPS last-expr) web-display)] [(CPS-code) ; happens when there is no non-definitions at the end (begin)])) ; 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?) 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. ========================================================================