« Coder to Co-Founder: Etech Tutorial | Main | O'Reilly Radar: ETech 2007 »
Applied Web Heresies: ETech 2007
I really wanted to go to Putting the Fun in Functional: Applying Game Mechanics to Social Software by Amy Jo Kim, but my inner geek won out and I went to Applied Web Heresies with Avi Bryant (slides). I hope someone else took good notes.
The basis for the talk is Seaside, a web framework for Smalltalk that Avi wrote several years ago. The problem with Seaside is you’re not going to use it! There are a lot of interesting ideas in Seaside that people should know, so this tutorial is way of spreading the ideas outside of Smalltalk.
Avi suggests using Seaside as a recipe. He tells the story of Primo Levy and onions in the varnish. There are a lot of “best practices” of Web development that were good decisions at the time but which are no longer needed.
Here’s the basic requirement list:
- OOP
- Servlet-style app server
- Blocks and closures
“First thing we do, let’s kill all the templates.” Templates were a good idea that have become useless and harmful. They’re constraining or they’re a bad programming language. The is a belief that templates are useful for model/view separation. But HTML is now a semantic layer and CSS is the real view layer (see Zen Garden). This is an interesting point of view and one I have a hard time arguing with.
So, you need a rich library for generating HTML (see AWT for inspiration). Each widget has a render method that gets a canvas passed to it.
The first thing we did was write a small framework to get things going. Different groups were working in different languages. I used Ruby (even though I don’t really know Ruby). Here’s mine:
require 'webrick'
require 'stringio'
server = WEBrick::HTTPServer.new(:Port => 2000)
server.mount_proc("/heresy"){|req, res| Application.new.handle(req, res)}
server.mount_proc("/favicon.ico"){|req,res| res.status = 404}
class Application
def handle(req, res)
canvas = Canvas.new(res)
render_on(canvas)
end
def render_on(html)
html.heading("Hello World")
end
end
class Canvas
def initialize(res)
@res = res
res['Content-Type'] = 'text/html'
end
def heading(str, level=1)
@res.body = "<h1>"+ str + "</h1>"
end
end
trap("INT"){ server.shutdown }
server.start
We’re omitting tag objects here and just spitting out HTML, but a real API should do that.
The next heresy that Avi proposes is that sessions are two valuable to persist. In general, you can never marshall and unmarshall the stuff in memory reliably. All the good stuff’s in memcached anyway. Keep the session in the memory of the application server
What about load balancing? Use sticky sessions. YAGNI Application servers going down is unusual. Users losing session data is a minor annoyance. Live with it.
NeXT’s WebObjects is the inspiration for much of what Avi’s done.
Next we move from our HelloWorld application to something that has stateful sessions. We’re going to build a registry of sessions and push the canvas down a level so that the sessions hold the canvas. My Ruby coding skills were not up to keeping up, so I cheated and grabbed Avi’s code (which includes a much more usable Canvas object):
require 'webrick'
require 'stringio'
server = WEBrick::HTTPServer.new(:Port => 2000)
server.mount_proc("/heresy"){|req, res| Application.new.handle(req, res)}
server.mount_proc("/favicon.ico"){|req,res| res.status = 404}
class Registry
def initialize
@items = []
end
def register(item)
@items << item
(@items.size - 1).to_s
end
def find(key)
@items[key.to_i]
end
end
class Application
@@sessions = Registry.new
def handle(req, res)
session_cookie = req.cookies.detect{|c| c.name == "heresy"}
if(session_cookie)
session = @@sessions.find(session_cookie.value)
end
unless session
session = Session.new
res.cookies << WEBrick::Cookie.new("heresy", @@sessions.register(session))
end
session.handle(req, res)
end
end
class Canvas
def initialize
@io = StringIO.new
end
def tag(name, attrs={}, &proc)
@io << "<"
@io << name
attrs.each{|k,v| @io << " #{k}='#{v}'"}
@io << ">"
proc.call
@io << "</#{name}>"
end
def text(str)
@io << str
end
def heading(txt, level=1)
tag("h#{level}"){text(txt)}
end
def string
@io.string
end
end
class Session
def initialize
@count = 0
end
def handle(req, res)
@count += 1
html = Canvas.new
render_on(html)
res.body = html.string
res["Content-Type"] = "text/html"
end
def render_on(html)
html.heading("Hello World: #{@count}")
end
end
trap("INT"){ server.shutdown }
server.start
What's left to do on session? Lots, including:
- Unguessable session keys
- Session keys as query params
- Session locks
- Expiration from registry
The next piece of heresy: meaningful URLs don't carry enough meaning. People put a lot of energy trying to create names that describe the particular point in an application. Not every page in an application is a meaningful part of an API. URLs, particularly query parameters are classic place where people repeat themselves. Don't repeat yourself. Lots of meaningless names create namespace collisions.
Avi proposes using a registry to store IDs against page names. The inspiration for this is TCL/TK: Register closures/blocks as callback objects. Here's the code that implements this refinement (I did it almost all by myself--I had to peak to see how WeBrick handled requests):
require 'webrick'
require 'stringio'
server = WEBrick::HTTPServer.new(:Port => 2000)
server.mount_proc(“/heresy”){|req, res| Application.new.handle(req, res)}
server.mount_proc(“/favicon.ico”){|req,res| res.status = 404}
class Registry
def initialize
@items = []
end
def register(item)
@items << item
(@items.size - 1).to_s
end
def find(key)
@items[key.to_i]
end
end
class Application
@@sessions = Registry.new
def handle(req, res)
session_cookie = req.cookies.detect{|c| c.name == “heresy”}
if(session_cookie)
session = @@sessions.find(session_cookie.value)
end
unless session
session = Session.new
res.cookies << WEBrick::Cookie.new("heresy", @@sessions.register(session))
end
session.handle(req, res)
end
end
class Canvas
def initialize(cbs)
@io = StringIO.new
@callbacks = cbs
end
def tag(name, attrs={}, &proc)
@io << "<"
@io << name
attrs.each{|k,v| @io << " #{k}='#{v}'"}
@io << ">"
proc.call
@io << "</#{name}>"
end
def text(str)
@io << str
end
def link(name, &proc)
id = @callbacks.register(proc)
tag("a",{ :href=>"?#{id}"}){text(name)}
end
def space
@io << " "
end
def heading(txt, level=1)
tag("h#{level}"){text(txt)}
end
def string
@io.string
end
end
class Counter
def initialize
@count = 0
end
def render_on(html)
html.heading("Hello World: #{@count}")
html.tag("p"){html.text("this is fun!!!")}
html.link("--"){@count -= 1}
html.space
html.link("++"){@count += 1}
end
end
class Session
def initialize
@callbacks = Registry.new
@root = Counter.new
end
def handle(req, res)
req.query.each do |k,v|
if callback = @callbacks.find(k)
callback.call(v)
end
end
html = Canvas.new(@callbacks)
@root.render_on(html)
res.body = html.string
res["Content-Type"] = "text/html"
end
end
trap("INT"){ server.shutdown }
server.start
As we finished this segment, I said "I get what we're doing, but why are we doing it?" In other words, why go to all this trouble to create a registry of callback methods and URLs that point to it uniquely? The answer is in the next little exercise. Here's the code I produced:
require 'webrick'
require 'stringio'
server = WEBrick::HTTPServer.new(:Port => 2000)
server.mount_proc(“/heresy”){|req, res| Application.new.handle(req, res)}
server.mount_proc(“/favicon.ico”){|req,res| res.status = 404}
class Registry
def initialize
@items = []
end
def register(item)
@items << item
(@items.size - 1).to_s
end
def find(key)
@items[key.to_i]
end
end
class Application
@@sessions = Registry.new
def handle(req, res)
session_cookie = req.cookies.detect{|c| c.name == “heresy”}
if(session_cookie)
session = @@sessions.find(session_cookie.value)
end
unless session
session = Session.new
res.cookies << WEBrick::Cookie.new(“heresy”, @@sessions.register(session))
end
session.handle(req, res)
end
end
class Canvas
def initialize(cbs)
@io = StringIO.new
@callbacks = cbs
end
def tag(name, attrs={}, &proc)
@io << “<”
@io << name
attrs.each{|k,v| @io << ” #{k}=’#{v}’”}
@io << “>”
proc.call
@io << “</#{name}>”
end
def text(str)
@io << str
end
def link(name, &proc)
id = @callbacks.register(proc)
tag(“a”,{ :href=>”?#{id}”}){text(name)}
end
def space
@io << “ ”
end
def heading(txt, level=1)
tag(“h#{level}”){text(txt)}
end
def string
@io.string
end
end
class MultiCounter
def initialize
@counters = [Counter.new, Counter.new, Counter.new]
end
def render_on(html)
@counters.each{|ea| ea.render_on(html)}
end
end
class Counter
def initialize
@count = 0
end
def render_on(html)
html.heading(“Hello World: #{@count}”)
html.tag(“p”){html.text(“this is fun!!!”)}
html.link(“—”){@count -= 1}
html.space
html.link(“++”){@count += 1}
end
end
class Session
def initialize
@callbacks = Registry.new
@root = MultiCounter.new
end
def handle(req, res)
req.query.each do |k,v|
if callback = @callbacks.find(k)
callback.call(v)
end
end
html = Canvas.new(@callbacks)
@root.render_on(html)
res.body = html.string
res[“Content-Type”] = “text/html”
end
end
trap(“INT”){ server.shutdown }
server.start
The only changes here are the addition of a MultiCounter class that creates a three item array of counter objects and a render_on object that calls render_on on each member of the array. Then, instead of putting a Counter object at the root, I put a MultiCount object. And you get three of the Counter widgets on the same page. This shows how easy it is to use the objects as Web widgets. Avi says you can’t do this with named URLs.
Avi remarks that pages are a lousy unit of reuse and partials ain’t much better. “But every piece of my application is a beautiful and unique snowflake.” Again, with the CSS.
What aren’t we doing?
- Splitting callback registries up by page view
- Tracking the back-button
- Redirecting after side-effects
- Forms!
Overall this is a very interesting set of thoughts about Web development. Clearly he’s espousing a lot of new ideas, which generated a lot of “How do you …?” questions. Very good stuff. This tutorial made me think, which is the best kind.
Posted by windley on March 26, 2007 2:56 PM




Comment from Ernest Jones at March 26, 2007 10:51 PM
I'd be interested to hear more about solving the "Unguessable session keys" problem you mentioned.
It reminds me of a related issue that the Erlang crowd is noodling on: creating un-forgeable process ids.
Comment from David Ganzhorn at March 27, 2007 9:34 PM
Wow, wonderful stuff. The code is so short, but so spiffy. I took it and started to play. Something about it seems very funny to me, a sort of programing joke.
My buddy and I implemented a simple tic-tac-toe game on top of the version with a callbacks registry, in a few dozen more lines of code. Very surprising how quickly it came together.
I'm tempted to start a website for the creations we make with this fun new toy. It's much better suited to making games than rails is.
Comment from Interested at March 27, 2007 10:38 PM
What other languages were represented? Anything interesting? Any functional takers?
Comment from todd at March 28, 2007 9:00 PM
David
Here is a rough take on tick tack toe
what does yours look like?
class TickTackToe
def initialize
@board = [[".",".","."],[".",".","."],[".",".","."]]
@turn = "X"
@result = ""
end
def render_on(html)
html.heading("Tick Tack Toe")
html.tag("p"){html.link("start") {initialize}}
html.tag("table") {
3.times { |row|
html.tag("tr") {
3.times { |col|
html.tag("td") {
html.link(@board[row][col]) { do_turn(row, col) }
}
}
}
}
}
html.tag("p"){html.text(@result)}
end
def do_turn(row, col)
@board[row][col] = @turn
@turn = @turn == "X" ? "O" : "X"
#check result - not optimized
@board.each { |row|
if row[0] != "." && (row[0] == row[1]) && (row[0] == row[2])
@result = "#{row[1]} won"
end
}
3.times {|col|
if @board[0][col] != "." && (@board[0][col] == @board[1][col]) && (@board[0][col] == @board[2][col])
@result = "#{@board[0][col]} won"
end
}
if @board[0][0] != "." && (@board[0][0] == @board[1][1]) && (@board[0][0] == @board[2][2])
@result = "#{@board[0][0]} won"
end
if @board[0][2] != "." && (@board[0][2] == @board[1][1]) && (@board[0][2] == @board[2][0])
@result = "#{@board[0][2]} won"
end
end
end
Comment from David Ganzhorn at March 28, 2007 11:40 PM
todd,
Mine is very similar. It has a bit of CSS tossed in which makes it a bit more cluttered.
Code paste here: http://rafb.net/p/ksxPkh83.html
I also made a very basic todo-list, no persistence, not even complete/incomplete status, but it uses forms and dynamically adds new widgets, and that was my main purpose.
Code paste here: http://rafb.net/p/KNwKU619.html (this includes the framework code, as I had to modify the canvas and session a bit to support forms.
Comment from Marko Samastur at March 29, 2007 12:01 AM
The idea to create a toolkit that would render web pages is certainly not new. I did something like this 5 or so years ago (Google for pyortal) and I seriously doubt I was the first one who did.
The problem of going GWT style is that it sounds very nice in theory, but breaks down horribly in practice. The reason for this is that it's a tedious way to write text into a page (just try doing it with something that has a fair amount of strongs and ems embedded).
But if you try to factor that part out and handle text differently, what you end up with is more or less a layout skeleton, which doesn't do you much either. There just aren't all that many widgets that would be complicated enough to warrant their own function call and would get used repeatedly.
Comment from todd at March 29, 2007 6:34 PM
Thanks David
Very cool
Comment from Sam Aaron at April 14, 2007 11:35 AM
Thanks very much for this write-up Phil. It was really interesting to follow through. Thought-provoking stuff :-)
Comment from Frank Meyer at October 17, 2007 11:28 PM
I also made a very basic todo-list, no persistence, not even complete/incomplete status, but it uses forms and dynamically adds new widgets, and that was my main purpose.
Leave a comment
I encourage you to leave a comment below. Your email address will not be displayed on Technometria, but allows me to communicate with you directly. Your email address won't be displayed, but will be used to compute a MicroID for your comment.