Ruby, HTML, s-expressions, lambdas? Oh my!
I've been noodling around with various lisps lately, and one of the sources of lisp's elegance is the lack of separation between data and code. A practical example of that is the cl-who library which creates html from an s-expression representation, ensuring all tags are correctly closed. For example:
(with-html-output (*http-stream*) (loop for (link . title) in '(("http://zappa.com/" . "Frank Zappa") ("http://marcusmiller.com/" . "Marcus Miller") ("http://www.milesdavis.com/" . "Miles Davis")) do (htm (:a :href link (:b (str title))) :br)))
There are a number of ways to replicate this style in other languages. You can do it by building up a big string, but I want to print to a stream like the example above. Using lambdas we can make a pretty good approximation in Ruby. Here's some Ruby code using a simple module I whipped up (see below) to achieve the same output as the cl-who lisp code above:
engines = [["http://google.com/", "Google"], ["http://yahoo.com/", "Yahoo!"], ["http://webcrawler.com/", "Showing my age"]] Htm.htm(engines.map { |e| Htm.htm(:a, :href, e[0], [:b, Htm.str(e[1])], :br, nil)}).call
Lets say we realise that the br tags are nasty and want to put the list of links into a list:
Htm.htm(:ul, engines.map \ { |e| Htm.htm(:li, [:a, :href, e[0], [:b, Htm.str(e[1])]])}).call
It's not quite as elegant as the lisp equivalent because we are mixing lambdas and arrays - both as nesting constructs. Here is the final output (indented for clarity - the Htm module below does no indenting):
<ul> <li><a href="http://google.com/"><b>Google</b></a></li> <li><a href="http://yahoo.com/"><b>Yahoo!</b></a></li> <li><a href="http://webcrawler.com/"><b>Showing my age</b></a></li> </ul>
Now even though we are controlling the order of execution such that the various parts will be printed to the stream in the right order, we still can't print until we have closed our outermost tag (usually html). To be able to stream output while also auto-closing the tags we're going to need to use continuations - I'll cook up an example of that for a later blog. There's also other neat ways we can have self-closing tags that will allow streaming - I'll post about that later also.
Here is the Htm module in full:
require 'cgi' module Htm def Htm.str(content) lambda { print CGI.escapeHTML(content.to_s) } end def Htm._build_tag(tag, args) # open the tag print '<' + tag.to_s tok = args.shift # find any tag attribute key/value pairs while tok.kind_of? Symbol print ' ' + tok.to_s + '="' + CGI.escapeHTML(args.shift.to_s) + '"' tok = args.shift end if tok.nil? # self-close tag if no content print ' />' else # finish open tag print '>' # output tag content Htm._eval_content( tok.kind_of?(Symbol) ? Htm.htm(tok, args) : tok ) # close tag print '</' + tag.to_s + '>' end end def Htm._eval_content(content) while content.kind_of? Proc content = content.call end if content.kind_of? Array Htm._eval_content(Htm.htm( *content )) elsif ! content.nil? print content end end def Htm.htm( *args ) lambda do tok = args.shift while ! tok.nil? if tok.kind_of? Symbol Htm._build_tag(tok, args) else Htm._eval_content(tok) end tok = args.shift end end end end
09:13 PM, 06 May 2009 by Mark Aufflick Permalink | Short Link







