Sunday, February 17, 2008

Seaside-Style Programming in ErlyWeb

The Arc Challenge started an interesting thread in the ErlyWeb mailing list about continuations-driven web frameworks. ErlyWeb doesn't have built-in support for continuations, but Arc does and so does Seaside. I haven't paid much attention to the use of continuations in web frameworks before the Arc challenge, but I became especially interested in experimenting with them after seeing Seaside solution.

In case you haven't read it, this is the requirement of the Arc challenge:

Write a program that causes the url said (e.g. http://localhost:port/said) to produce a page with an input field and a submit button. When the submit button is pressed, that should produce a second page with a single link saying "click here." When that is clicked it should lead to a third page that says "you said: ..." where ... is whatever the user typed in the original input field. The third page must only show what the user actually typed. I.e. the value entered in the input field must not be passed in the url, or it would be possible to change the behavior of the final page by editing the url.

This is the original Arc solution:

(defop said req
(aform [w/link (pr "you said: " (arg _ "foo"))
(pr "click here")]
(input "foo")

This is how you would write the same logic in Erlang, if it had an Arc-like web framework:

said(A) ->
fun(A1) ->
link(fun(A2) -> ["you said ", get_var(A1, "foo")] end,
"click here")

The Erlang code is a bit more verbose, mostly because Erlang macros don't allow you to hide the "fun() -> ... end" syntax the way Lisp macros let you hide the (lambda ..) keyword.

This is the Seaside solution:

| something |
something := self request: 'Say something'.
self inform: 'Click here'.
self inform: something

IMO, this solution cheats a bit by using high-level functions for generating the HTML tags whereas the Arc version generates them explicitly. However, putting minor complaints aside, I think the Seaside version is the winner in readability. As a reddit comment said, it reads like prose. It doesn't even declare any closures explicitly -- it says exactly what it does and nothing more.

Wouldn't it be cool if we could use this style of programming in ErlyWeb applications?

Fortunately, we can! I hacked a continuations plugin for ErlyWeb that lets you write Seaside-style code so fans of this programming style would feel at home with ErlyWeb. (This is all done in 105 lines of code :) ) Before I explain how the plugin works, I'll show you how to create an ErlyWeb controller that implements the the Arc challenge using this plugin:

-import(continuations, [ask/2, confirm/2, pr/1]).

index(A) ->
A, fun(K) ->
Name = ask(K, "name"),
confirm(K, "click here"),
pr(["your name: ", Name])

This may seem more verbose than the Seaside code because of the module declarations at the top, but the "meat" is about the same. (I could make this code even smaller by integrating continations.erl deep into ErlyWeb, which would remove the explicit call to continuations:do(). However, I didn't want to go too far with this proof of concept.)

How does this work? Using concurrency, of course! For each "continuation", the plugin spawns a process and registers it in a Mnesia table according to a randomly generated key. The key (K) is encoded in the URLs to which the <form> and <a> tags point. When a request arrives, continuations:do() looks up the process in Mnesia and sends it a message of the type {A, self()}. The process does some work and sends back in reply the HTML to be rendered, and waits for the next message. The web server process receives the rendered HTML and sends it to the client using the normal ErlyWeb mechanisms.

If a process doesn't receive a message in 10 seconds, it dies and removes itself from the Mnesia table, which provides automatic garbage collection to stale sessions.

You can get the code for continuations.erl here. Just remember it's a proof of concept and I don't recommend using it in a production environment.

(Before you use it in your application, make sure you call continuations:start().)

Final word: IMO, although continuations help write more natural code in certain multi-page interactions, most of the logic in web applications involves rendering dynamic pages for RESTful URLs. So, if your web framework doesn't support continuations, don't worry about it too much. It's likely the code for your application wouldn't be dramatically smaller if you could write it with the use of continuations. (That said, take my advice with a grain of salt. I haven't used a continuations based framework to build a real application, so I may be missing something.)


Akshay Surve said...

I have been following your blog for a month now and also silently following the ErlyWeb Google group.

I remember Paul initiating the Arc Challenge and throwing it open. I thought it would be great to see Erlang/ErlyWeb way of doing it and there you were giving it a shot. And now following it up with this code sample which is just awesome. I really appreciate your your work.

Played around a little with Erlang and raring to explore ErlyWeb. :)

Magnus Falk said...

Slava Akhmechet has yet another Web framwork where continuations is one of the main features (this one made in Common Lisp) and he writes some really good articles on the subject. He also seems to be a bit of an Erlang fan to boot!

Moot said...

Beware of the patents! Continuation-based servers are patented by PG and the patent is owned by Y! (soon, MS)

Dan said...

Beware of the patents! Continuation-based servers are patented by PG and the patent is owned by Y! (soon, MS)

From Wikipedia, on US patent law:
Thus, merely thinking about an invention, or drawing a diagram, is not an infringement. Research for "purely philosophical" inquiry is not an infringement, but research directed to commercial purposes is - unless the research is directed toward obtaining approval of the Food and Drug Administration for introduction of a generic version of a patented drug.

So open source platforms, or little experiments like this are fine.

Mathias said...

Quite nice, though you don't have continuations. The elegance of continuations is that the user can use the back-button freely, and the continuation corresponding to the page he resumes working on will be the target of any action.

Using processes, what if the user moves two pages back, and resubmits a value? Your process and user input are in two different ... places.

e.g. the simple example of two pages asking for a number each, and a third one displaying the sum will not work. Entering 1 on the first, and moving to the second page one cannot go back to the first page to correct the number to 3, because the process expects second number, not the first.

You would need to have some sort of automagic undo mechanism to get this to work with processes.

Too bad, this would be an elegant solution.

Raoul Duke said...

i'm thick. i don't see how this really has anything to do with continuations. cannot one simply use a hidden form field to pass the data along through the intermediate page?

if there was a requirement that the user could quit the browser and come back a day later and get the results, then that would be more in the vein of continuations?