Continuation based web server in Factor

Chris Double — 2004-06-22 23:49:45

I've implemented a proof of concept continuation based web server in
Factor. For information on continuation based web servers see:

http://radio.weblogs.com/0102385/2003/08/30.html#a422
http://www.double.co.nz/scheme/modal-web-server.html

Draft of the implementation is below. Comments welcome.

----------------------8<---------------------
In a continuation based web application the current position within
the web application is identified by a random URL. In this example the
URL is generated as a string of random digits:

: get-random-id ( -- id )
"" 16 [ random-digit >str cat2 ] times ;

Each URL is associated with a word or quotation. When that URL is
accessed, that quotation is executed. The quotation will receive a
table on the top of the stack which holds any POST or query parameters
(not currently implemented in this proof of concept). A global table
is maintained that holds this association:

: continuation-table ( -- <table> )
#! Return the global table of continuations
"cont" get ;

: reset-continuation-table ( -- )
#! Create the initial global table
<table> "cont" set ;

: register-continuation ( cont -- id )
#! Store a continuation in the table and associate it with
#! a random id.
continuation-table [ get-random-id dup [ set ] dip ] bind ;

: get-registered-continuation ( id -- cont )
#! Return the continuation associated with the given id.
continuation-table [ get ] bind ;

: resume-continuation ( value id -- )
#! Call the continuation associated with the given id,
#! with 'value' on the top of the stack.
get-registered-continuation call ;

When a an URL is accessed, the continuation for the specific URL is
obtained and called. That continuation needs to exit back to the
caller when it has some HTML that it needs to display. returning that
HTML to the caller. To exit back to the caller it calls an 'exit
continuation' that is stored in an "exit" variable:

: exit-continuation ( -- exit )
#! Get the current exit continuation
"exit" get ;

: call-exit-continuation ( value -- )
#! Call the exit continuation, passing it the given value on the
#! top of the stack.
"exit" get call ;

: with-exit-continuation ( [ code ] -- )
#! Call the quotation with the variable "exit" bound such that when
#! the exit continuation is called, computation will resume from the
#! end of this 'with-exit-continuation' call, with the value passed
#! to the exit continuation on the top of the stack.
[ "exit" set call call-exit-continuation ] callcc1 nip ;

All this calling of continuations is hidden behind a single 'show'
call. 'show' will take a quotation on the stack. That quotation should
return an HTML string. 'show' will call it to generate the HTML and
call the exit continuation with this string on the stack so it gets
returned to the httpd responder. The quotation receives a 'url' on the
top of the stack which is the 'id' of the continuation to resume when
that URL is accessed.

To generate the string of HTML I use 'with-string-stream' which calls
a quotation and all output inside that call gets appended to a string:

: with-string-stream ( quot -- string )
#! Call the quotation with standard output bound to a string output
#! stream. Return the string on exit.
<namespace> [
"stdio" <string-output-stream> put call "stdio" get stream>str
] bind ;

: show ( [ code ] -- )
#! Call 'code' with the URL associated with the current
#! continuation. Return the HTML string generated by that code
#! to the exit continuation. When the URL is later referenced then
#! computation will resume from this 'show' call with a <table> on
#! the stack containing any query or post parameteres.
[
register-continuation swap with-string-stream
call-exit-continuation
] callcc1
nip ;

An httpd responder is used that takes the ID as an argument, retrieves
the continuation associated with it and calls it:

: cont-responder ( id -- )
#! httpd responder that retrieves a continuation and calls it.
[ <table> swap resume-continuation ] with-exit-continuation
serving-html print drop ;

Some code to install the responder:

"httpd-responders" get [
<responder> [
[ cont-responder ] "get" set
] extend "cont" set
] bind

Now to test it. Here's a function that displays some text on an HTML
page:

: display-page ( title -- )
[
swap [
"<a href='" write write "'>Next</a>" write
] html-document
] show ;

Note that it contains an A HREF link to the URL that resumes the
computation (The quotation passed to show has this URL passed to it by
show).

A word that displays a sequence of these pages would be:

: test
"Page one" display-page
"Page two" display-page
"Page three" display-page ;

Now we register this word with the continuation system:

[ test ] register-continuation

This returns an ID which can be used from the web server to resume
it. Start the web server:

8888 httpd

Now access the URL with that id:

http://localhost:8888/cont/1234567890

(where 1234567890 is the ID returned by register-continuation above)

You'll see the first page and a link. Click the link and you'll see
the second page. Ditto with the third. You can go back and forth with
the back button, refresh, bookmark, etc as expected.

Note that this is just a proof of concept. In a real framework the
continuations need to be expired after time. It would also enable
generating links to other pages, etc rather than just a sequence of
pages. Implementing POST is also an important addition to make things
useful. I plan to flesh this out over the next few days and present
some more useful examples. My main point was to see if it was possible
to do this type of thing in Factor.

The number of words required to get things going is amazingly small
and it was very easy to develop this far.

If you want to try the above I used the following 'uses':

USE: stdio
USE: unparse
USE: httpd
USE: httpd-responder
USE: random
USE: continuations;
----------------------8<---------------------
--
Chris Double
chris.double@...

Chris Double — 2004-06-23 21:09:34

I've updated this example now. The description is at:

http://www.double.co.nz/factor/cont.txt

and the code in:

http://www.double.co.nz/factor/cont-responder.factor

The description contains instructions on how to use it with examples.
The nice thing about this type of server is the standard concatenative
operations work as expected:

: test-cont-responder2 ( alist - )
#! Test the cont-responder responder by displaying a few pages in a
loop.
[ "one" "two" "three" "four" ] [ display-page ] each
"Done!" display-page ;

'display-page' causes a web page to be displayed with the top most item
on the stack on the page and a 'Next' anchor which takes the user to the
next stage of the computation. The snippet above will display each item
on the list in seperate pages with the user navigating between them
using the back button or 'Next'.

This version also includes POST support. You can have a FORM on a page
and the results are returned as an alist. An example of use is:

: display-get-name-page ( -- name )
#! Display a page prompting for input of a name and return that name.
[
"Enter your name" [
"<form method='post' action='" write write "'>" write
"Name: <input type='text' name='name' size='20'>" write
"<input type='submit' value='Ok'>" write
"</form>" write
] html-document
] show
"name" swap assoc ;

: test-cont-responder ( alist - )
#! Test the cont-responder responder by displaying a few pages in a
row.
drop
"Page one" display-page
"Hello " display-get-name-page cat2 display-page
"Page three" display-page ;

Here 'display-get-name-page' displays a page prompting for the name, and
returns it by extracting it out of the alist returned by 'show'. 'Hello
' is concatenated to it and it is displayed on an other page. These
pages can be bookmarked, revisited, etc.

Slava, I implemented the breaking up of the POST data in the following
word:

: post-request>alist ( post-request -- alist )
#! Return an alist containing name/value pairs from the
#! post data.
dup "&" swap str-contains [
"(.+)&(.+)" groups [ "(.+)=(.+)" groups uncons car cons ] inject
] [
"(.+)=(.+)" groups uncons car cons unit
] ifte ;

It's called like:

read-post-request post-request>alist

I'm not sure if I need the decode-url there? I suspect I need to decode
the name and value parts of the POST data.

Upcoming improvements will be allowing anchors to arbitary quotations,
callbacks, and some examples.

Chris.
--
Chris Double
chris.double@...