Clementson's Blog

Bits and pieces (mostly Lisp-related) that I collect from the ether.

July 2005
Sun Mon Tue Wed Thu Fri Sat
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Jun  Aug

Continuations in Uncommon Web

Sunday, July 31, 2005

I've mentioned Uncommon Web (UCW) a few times in the past few days. When I've referenced UCW, I've usually indicated that it is a continuation-based web application framework. Why "continuation-based"? Having a continuation-based web application framework provides benefits for the developer at two different stages in the development of a web application - development-time and run-time (as Marco explains in this email exchange with Dave Robers that I've edited slightly):

  1. Writing the code: We (some of us at least) would like to write our code sequentially, even though it will be executed asynchronously and certain parts of it may be executed multiple times (think back button and window cloning). There is no penalty here if the continuations are implemented by a compile time code transformation (as UCW does). [well, there is a slight penalty since we introduce an obscene number of lambda calls, but it's basically 0 compared to reading/writing the requests/response (trust me on this)]. Even when you use "full" built-in continuations (as SeaSide and PLT scheme do) the overhead is still little more than a couple function calls and some memory (stack) manipulation.

  2. Saving the state: When the user clicks on a link we want to make sure that the state of the app as the user saw it on the page corresponds to the state on the server, even when the user has used the back button. In this case you, developer, have to tell the framework what state shoud be saved and restored based on the user's request and what state is global and doesn't change even though the user has "undone" some pages.

    Generally we save the state regarding the GUI (values of forms, what the "current" page is, state of the navigation menu) and don't save the general application state (user data, db manipulations, etc.) how much you save is up to the developer. I'd like to point out that this penalty (saving and restoring GUI state) is taken even by REST apps (if they want to do things right), it's just that's its done by hand. The difference is who has to deal with that job. In a continuation-based system you usally end up with a framework where the saving and restoring is done by the framework itself, all you (the developer) have to do is use tha data. in the REST frameworks i've seen you have to do this work yourself.
For those of you unfamiliar with UCW, you might question how it can be "continuation-based" when CL doesn't support continuations. Well, Marco Baringer (the creator of UCW) has written a CPS transformer that saves the lexical and GUI state in a web application written with UCW. This is described in greater detail in his Arnesi package documentation. However, it is worth noting that UCW does not support full continuations, it only supports the use of continuations with a subset of Common Lisp. It is interesting to explore how this use of continuations differs from having them available natively in other languages. Although you may lose the benefit of being able to use continuations anywhere, you retain the ability to use unwind-protect in your CL code (a trade-off that has been discussed by both Scheme and CL proponents in the past).

Larry D'Anna has written a brief example of how UCW continuations are similar to but different from Scheme continuations. His example code is reproduced below:
(in-package #:it.bese.arnesi)
;; I'll explain this below (eval-when (:compile-toplevel :load-toplevel :execute) (setq *call/cc-returns* t))
;; Arnesi continuations are not quite identical to scheme ;; continuations. Since only part of the program is translated into ;; cps it is not really possible for a continuation to represent the ;; entire future of the computation. Instead it represents the future ;; of the computation, up to the point that we most recently entered ;; cps-transformed code. (or called a continuation from inside cps ;; code) Therefore the result of calling a continuation (or entering ;; cps-transformed code via with-call/cc or by calling a cps style ;; function or method) is not to abort our current computation and ;; switch to a different one like it is in scheme, but to initiate a ;; cps computation and return it's result to our current one. thus ;; the following snippet will print (foo bar) as well as (foo baz) ;; whereas in scheme it would only print (foo bar) because in arnesi ;; the call to the continuation returns, whereas in scheme it never ;; does. (with-call/cc (print (list 'foo (let/cc k (k 'bar) 'baz))))
;; Also note that the entire form returns (foo baz) and the ;; discarded return value of (k 'bar) is (foo bar). ;; Now to explain the stuff at the top: In scheme if you run (call/cc ;; x) and x returns a value then (call/cc x) simply returns that same ;; value. This is not the default behavior or arnesi, although ;; setting *call/cc-returns* to t will provide it. The default ;; behavior is abort to the most recent "cps entry-point" and return ;; the value from there. This is useful for implementing coroutines ;; because if calling a continuation doesn't abort a computation then ;; you pretty much have no other way of doing so. With ;; *call/cc-returns* set to nil the above computation will only print ;; out (foo bar) and it will return baz (eval-when (:compile-toplevel :load-toplevel :execute) (setq *call/cc-returns* nil))
(with-call/cc (print (list 'foo (let/cc k (k 'bar) 'baz))))
;; Here is an example of how to use this construct to implement coroutines ;; It should print out the first 10 pyramid numbers. (defmacro coro (&body body) (with-unique-names (coro) `(let (,coro) (with-call/cc (flet ((yield (x) (let/cc k (setq ,coro k) x))) (yield :ok) ,@body)) #'(lambda (x) (funcall ,coro x)))))
(let* ((tri-cr (coro (loop with num = 0 for i = 1 then (1+ i) do (yield (incf num i))))) (pyr-cr (coro (loop with num = 0 do (yield (incf num (funcall tri-cr nil))))))) (loop for i from 1 to 10 collect (funcall pyr-cr nil)))

Update-2005-08-01: Pascal Costanza pointed out via email (reproduced here with his permission) that
"... continuations and unwind-protect are _not_ incompatible. Kent Pitman made that claim on his website some time ago, but it's wrong and he has even retracted the claim (but didn't update the original paper yet).

The only thing necessary is that unwind-protect has to communicate with "active" continuations whether they should trigger the protect forms or not, i.e. whether they are escaping or non-escaping continuations, and that's relatively straightforward to do. Indeed, it would be easier to add call/cc to Common Lisp than to add unwind-protect to Scheme, because all existing constructs that call continuations in Common Lisp are (by definition) escaping, so one would only have to add non-escaping continuations. In Scheme, there is no provision to distinguish between escaping and non-espacing continuations, so adding unwind-protect would probably require to change all "legacy" Scheme code."
He also provided a link to Will Clinger's Scheme code for implementing unwind-protect in Scheme.

emacs Copyright © 2005 by Bill Clementson