Modal web frameworks offer a simpler and more natural way to implement some web workflows in code. They've been around for decades and a few of them are written in Ruby. So, why aren't we using them? I wanted to find out, so I made my own.
Wouldn't it be swell if you could write your web controllers like this?def delete text = "Enter the name of the record you want to delete" loop do action, record = input :text => text, :buttons => {:delete => "Delete", :cancel => "Cancel"} break if action == :cancel if record_exist? record delete_record record if dialog :text => "Really delete #{record}?", :buttons => {true => "Yes", false => "No"} alert :text => "Record deleted" break else text = "Record not found, please try again" end end end
Imagine that each of those calls to input, dialog and alert renders an HTTP response and doesn't return until an associated HTTP request comes in. This happens without blocking the rest of your application. The server can efficiently carry on many of these dialogs simultaneously, in a single process, with a single thread.
In fact, this style of web programming is quite possible. It's been dubbed modal web development, and at it's heart are continuations. A continuation is an all-powerful GOTO label that can be called at any time, from any place. Surprisingly, they are not considered harmful, and are actually considered really cool by the few people who are cool enough to know what they are.
There are a handfull of modal web frameworks out there, most notably Seaside, and now Web Velocity. There have been efforts to bring this style of framework to Ruby, out of which came IOWA and Borges among others, but interest seemed to die off around 2005 which is, not surprisingly, around the time that Rails made its smashing debut.
I don't know if there is some fatal flaw with this approach that has kept it relatively obscure, or if it's just too strange for the average code monkey. Avi Bryant, creator of Seaside, will be in town for Future Ruby so maybe I'll ask him.
Regardless, I wanted to get the gist of how modal web programming worked, so I whipped up a Sinatra extension that lets you do this:
require 'sinatra' require 'sinatra/strands' require 'haml' helpers do def message locals={} haml :message, :locals => {:uri => strand_uri }.merge(locals) end end strand_get '/' do show message :text => "Step 1: click this button", :button => "ok" show message :text => "Step 2: click it again", :button => "yup" show message :text => "Step 3: one more time baby", :button => "oh yeah" message :text => "Ok, you're done", :button => "again", :uri => '/' end __END__ @@ message %html %head %title A Poignant Message From The Internet %body %h1= text %a{:href => uri, :style => "border: 1px solid black; padding: 10px" }= button
strand_get/post/put/delete are wrappers around get/post/put/delete that create a Strand, which is a wrapper around Ruby 1.9's Fiber, a thread-like object based on continuations. Visiting the root of this app will let you step through the block passed to strand_get by clicking through a sequence of pages. Calls to the show helper yield a response from the strand and suspend it. Requests to /_strand/$id resume the strand with the given id, passing the request object which is returned by show. The resume URI is available from strand_uri.
This extension is not meant for practical use. It neglects many important issues and I'm probably doing it all wrong anyway. It's simply an experiment. But it does work and it's not hard to imagine how it could be further developed to run the earlier fake code.
I'm particularly inspired by the idea of an unobtrusive extension that would make this sort of thing available in Rails, or some other popular framework, alongside the existing event handling approach.
Update: Giles Bowkett gave up on the idea of continuations in Rails because you can't call them across threads. Indeed, this is a problem as a "strand" would need to be serviced exclusively by the thread it was created in. This could be arranged, but would cause terrible contention the instant the strand count exceeded the size of the thread pool.
However, we now have NeverBlock which can run Rails in a single thread and still handle very high concurrency. It does this in essentially same way we do modal web interaction, but directed at the database instead of the user. In fact, the two features could potentially share some code.
That’s some hot stuff. A Rails plugin could be sweet.
I think there are two major reasons modal frameworks never got big:
1. It’s quite a different way of thinking about making web apps, and people generally don’t bother to learn new things unless there’s an obvious advantage, or everyone else is using it.
2. It’s a bit voodoo; understanding an example-app’s code requires you to load a fair bit of implicit context into your head. It’s not obvious what assignments return instantly and which are magic. Of course, your modal Sinatra framework is much easier to read than the modal frameworks from 2005, so maybe it’s not such a big deal. :)
Regardless, it’s spiffy. A nice tool to have in your toolbox. :)
What I’d really like is a set of DSLs for defining high-level webapp structures – like wizards, search/browse/edit panes for records, etc.– and have the app generate them on the fly. RAD, baby!
Web development is incredibly messy and a fast moving target, which has made high level abstractions difficult. Any model that tries to cover all the subtly different cases is inevitably incomplete, too restrictive or too complicated.
Rails is successful because it doesn’t try to abstract. It just gives you a variety of tools to use at your discretion. Rather than try to hide the existence of some tedious and repetitve task, it provides a tedious_repetitive_task helper with a few parameters that covers 95% of cases, and it stays out of the way for the other 5% that have to do it by hand. Some consider this a compromise, but it’s the best approach we have right now, or at least the best that’s widely known.
I think the tricky part about a continuation extension to Rails will be managing the context that actions run in. Right now, they are methods of a controller object that represents a single request. If the action runs across multiple requests, the CGI related properties will be expected to change (request, params, cookies, etc), but not the instance variables or self (in fact, it’s impossible to change self). I’m still trying to figure out the best approach to that. It would help if I had a better idea of the kind of patterns that are used with modal web dev. I know how it works in theory but I’ve never actually built anything with it.