Decoding "Almost Sinatra"
Science! true daughter of Old Time thou art! Who alterest all things with thy peering eyes. Why preyest thou thus upon the poet's heart, Vulture, whose wings are dull realities? — Edgar Allen Poe
In 2010 Konstantin Haase, the lead developer of Sinatra, set about writing as minimal a version of the web framework as he could, using all the obfuscation and minification tricks in the Ruby book. The result was Almost Sinatra, a Sinatra-compatible framework in a frankly microscopic 999B of code, spread across a mere 7 lines.
It’s an impressive feat; the full code for Almost Sinatra is below:
Always keen to explore the darker arts of languages that I program in, I thought I’d break Almost Sinatra down, statement-by-statement, and figure out the tricks Konstantin has used. I hope by doing so I’m not somehow “unweaving the rainbow”, destroying the beauty in what is basically the coding equivalent of symbolist poetry, abstruse and thick with meaning; I hope, in other words, that it’s possible to have respect for a work of art while still inspecting its brush-strokes.
Setting up
Without further ado, let’s begin at the beginning with the first statement in the script.
There’s already some syntax here that might be unfamiliar to the Ruby beginner; so let’s reformat it to perhaps make it slightly clearer:
The first space-saving tip is a basic one: you can use the %w
literal
to create arrays of strings easily. So this:
…can be written as:
You can use any character you like for the delimiters; Konstantin has
used full-stops (.
), but it’s probably more common in the wild to see
some variety of brackets ([]
, ()
, {}
, and so on).
The other technique on display here is the reuse of loops. Two things
need to happen at the start of the script: first, Almost Sinatra’s
modest dependencies need to be require
d; and secondly, AS needs to
tell Ruby that it’s interested in trapping signals sent to the Ruby
process. In this case, the signals are SIGINT
(the signal sent when
you hit Ctrl + C
in the terminal) and SIGTERM
(the signal sent when
the process is killed).
This could be done in two steps: loop over the dependencies, requiring
each one; then loop over the signals to be trapped, passing each of them
to trap
. But when space is at a premium, two loops are a luxury.
So we can see the technique: the script is passing "rack"
, "tilt"
,
and "date"
to trap, which aren’t valid signals. What happens if we
pass something to trap
that isn’t a valid signal? We can see from
a REPL session:
[1] pry(main)> trap("rack") {}
ArgumentError: unsupported signal SIGrack
from (pry):1:in `trap'
The first three elements in the array, then, will trigger an
ArgumentError
. rescue
, when called as the modifier to a line line
this, will recover all exceptions; so, instead of trapping a signal, the
script will drop through to the rescue call require
with the array
element instead. Bingo: one loop, two purposes.
The actual handler in trap
stops the server cleanly. We’ll find out
more about $r
later, when it’s actually created: there’s a long way to
go first.
An even simpler statement for number two, but an important concept: here, a constant that’s used multiple times in the script is assigned to a one-letter variable. Although this seems like it might be a waste, the characters are made up if it’s even used once or twice more — or even quicker, if the constant has a particularly long name.
This is a curious technique. $z
is used as the port that Sinatra will
run on; in this case, Date.new.year
is called (which always returns
-4712
, the default value for years). 145 is added, taking us to -4567;
finally, the sign is ignored by calling abs
, leaving us with 4567
— the default Sinatra port.
This of course is identically and always equivalent to:
It seems that this shorter option was rejected, tongue-in-cheek, to discourage the use of “magic numbers”; it seems that sometimes even the principles of Golf can play second fiddle to the church of DRY.
A simple puts
statement, this does contain one neat trick: if you’re
accessing a sigiled variable (that is, a global that begins $
or an
instance variable that begins @
), you don’t need the curly braces in
the interpolation operator. So the commonplace:
can be expressed as:
Readability takes a hit, but a vital two characters are saved.
The main logic
The first line, which was mostly setup, is over; now we’re into the bulk of the logic. A new module is defined; this module will contain the Sinatra-related functionality.
Rack is mixed into the new module; this allows Rack methods to be used
without namespacing them. AS will go onto use Rack::Builder
to create the app, which offers with its map
method a space-efficient
way to do Sinatra’s URL mapping.
There are quite a few things going on in this patch of multiple assignment, but they’re simple enough individually.
First, a new instance of Rack::Builder
is initialised and put into
a
:
(It might help to think of a
as app
.)
Then, a shortcut is defined. The script will define a lot of methods
dynamically, so D
becomes an alias of define_method
to save lots of
characters later:
Strictly speaking it’s actually set to the method object of
define_method
, since Ruby doesn’t allow us to pass around functions as
values; but in practical terms it’s similar. After this shortcut is
defined, D.call(*) {}
is equivalent to define_method(*) {}
.
Finally, S is set to the regular expression needed to parse Sinatra’s inline templates:
Handling requests
Here’s where we start to see elements that are recognisably “Sinatra”:
this block defines the global methods for defining routes (get "/foo"
do
and so on). Let’s format it, expand some aliases, and rename some of
those one-letter variables, so we can see a bit more clearly what’s
going on:
Things start to look a bit simpler now. For each of the HTTP request
methods (using map
rather than each
, since that saves a character),
a global method is defined — each of which takes a path as its first
argument and then a block. These methods are defined using the
define_method
shortcut we saw before; it’s worth noting that, instead
of using D.call("method_name")
, D.("method_name")
is used — which is
shorter. You can actually go shorter still, since []
is an alias of
call
, and use D["method_name"]
.
When called, this created method uses the map
method of
Rack::Builder
to add a new route into the Rack app that matches the
path given. When executed, this handler will set the response status to
200 (“OK”), the content type to HTML, and the response body to the
return value of the block (which is executed in the context of the app
using instance_eval
).
This is, fundamentally, exactly the same way that classic Sinatra apps work.
Parsing templates
The next line concerns itself entirely with templates; it’s what allows
you to call, for example, haml :foo
and have the template foo.haml
rendered as the response body.
Again, this complicated block is much easier to understand if we let it breathe and give the variables some more verbose names. Starting with the first two statements:
We can expand them to:
Tilt is a library that acts as a frontend for many different templating languages; it’s used by regular Sinatra, too.
For each of the template languages that Tilt supports, this code defines
a global method (called e.g. erb
or haml
). This method does the
following:
The first part of this code extracts the inline templates from the current file and builds them up into a hash.
Amusingly, rather than creating an empty hash with {}
, as you’d
expect, AS calls:
_jisx0301
is a semi-private method of the Date
class that expects
a string in the JIS X 0301-2002 format — the standard used in Japan. If
it’s given one, it returns a hash with the year, month, and day of the
given date; but if the input is invalid, it returns an empty hash. So,
passing it “hash, please” is possibly the strangest and most brilliant
way to get an empty hash in Ruby — another odd joke, to go with the port
number, that only makes the final size of the script even more
impressive.
Next, AS fetches the inline templates that exist in the script that’s
calling Sinatra. It can’t rely on __FILE__
to figure out what this
file is, since that refers to the file that AS is being defined in which
might be different to the file where the Sinatra methods are
actually called. It also can’t use $0
, which refers to the script
that’s being executed, since that might be different again.
So it uses caller[0]
to look at the most recent entry in the call
stack, then parses out the filename. It then uses scan
with the regex
defined above to extract all of the inline templates and build them into
the h
hash, which is memoised into $t
; this memoisation means that
parsing will only happen once, rather than every time a template is
parsed.
Expanding it and using clearer variable names, we might rewrite this section as follows:
Next, the template is parsed:
Tilt allows you to ask for a particular type of template — “erb”, for
example — and get back the class that you need to initialise to parse
those templates. In AS’s case, that class is in v[0]
; it’s
initialised, and a block is passed to it containing the template data.
The render method is called, passed the app as its evaluation context,
and any locals that have been assigned are passed to it (or an empty
hash if there are no locals).
There’s nothing too interesting there.
Here more global methods are defined; we’re used to the D.(m)
trick by
now. You’ll notice, though, that the methods here are actually all the
same, and that none of them do anything other than executing the blocks
passed to them; this means that there’s no practical difference between:
and:
This is presumably not what Sinatra itself does, but provides enough
compatibility with it to get by. But it means you couldn’t do things
like configure
blocks based on environments in Almost Sinatra, but
really — that would be asking a bit much!
An END
block is specified, since this is shorter than the perhaps more
standard at_exit
; this is what actually runs the webserver. We see
that $r
is set to the running server instance, as we saw in our trap
call in the very first line.
A couple of tiny tricks are used here: the second argument to run
is
a hash, so the braces are omitted (perfectly valid and indeed fairly
common) and the Ruby 1.9 hash syntax is used, where Port: $z
is
equivalent to :Port => $z
.
The params
and session
methods are defined; these are passed through
to q
, which isn’t defined at this point but will later be set to the
request object. You might have noticed that, way back up in line two, we
saw a lopsided assignment, where four variables were given three values:
This has the effect of setting q
to nil
, defining it in this scope
without actually having to assign a value to it. It allows you to scope
a variable in two characters, rather than the minimum of three or four
you’d need to assign an actual value to it.
Sinatra includes Rack sessions that used cookies, and runs requests
synchronously using Rack::Lock
.
The before
method is defined; this delegates the logic to
Rack::Builder
, calling use
to load the middleware.
It then uses this before
handler to load middleware of its own:
…which sets q
to the current request, and then converts the keys in
the params hash to symbols.
Although this is technically the end of the script, you’ll recall that
the server actually runs in an END
block; so at this point, if there’s
no further script, the END
block will trigger and the server will
start.
That’s it; our journey through Almost Sinatra is complete. I don’t know
about you, but I learned quite a few things. I learned how to use
Rack::Builder
; that combining loops shortens code dramatically; that
method objects can be called with the shortened method.()
syntax as
well as method.call
; that "#$foo"
is equivalent to "#{$foo}"
; and
lots more besides.
But it’s not just about learning little tricks. Almost Sinatra’s GitHub page opens with a quote from _why:
Until programmers stop acting like obfuscation is morally hazardous, they’re not artists, just kids who don’t want their food to touch.
I think that really nails the feeling I get from reading through this code and from deciphering it. Obfuscation is sometimes a virtue: allusion is often more powerful than an outright statement. More than anything, though: it’s really quite fun.
Add a comment