The Wikipedia entry for "metaprogramming" defines it as:
(…) the writing of computer programs with the ability to treat programs as their data. It means that a program could be designed to read, generate, analyse and/or transform other programs, and even modify itself while running.
For the purposes of this article, we can define metaprogramming as the ability to write code that writes code. I’ve been dwelling on this topic a lot ever since I started playing with the Elixir language, however, after having discussed RESTful API design with my co-workers during the last few weeks, I set out to build a simple tool that allows one to easily explore and consume a web API. The result is a Ruby gem affectionately called Blanket. Here’s an example of Blanket in use, together with the Github API:
Dynamic method dispatching
Once you’ve wrapped an API with Blanket, every method you call on the wrapper is added as a part of the URL. Only when you call any of the action methods (
DELETE) is the request actually executed. If you pass a parameter to a method such as users, it is appended after it as a resource identifier, following the REST convention of
The actual code that does this is surprisingly small and terse, thanks to Ruby’s support for dynamic method dispatch via
method_missing. If you define this method in one of your classes, all messages sent to that object that are not statically defined get rerouted to the handler you’ve defined. At the time of writing, Blanket’s method missing reads as such:
Calling any undefined method on a
Blanket::Wrapper object will return a new instance wrapped with the current URL. The fact that these objects are, for all intents and purposes, immutable is an aspect that greatly lowers the complexity of the program, but that is enough material for another post ☺.
This class currently spans around 90 LOC, with most of it dedicated to private helper methods. The
Wrapper class contains five other public methods though: the RESTful request actions. These, however, are not statically defined in the code either, they are generated at runtime.
Code that writes code
Ruby objects have another very useful method for metaprogramming called
define_method. This allows you to define methods in your class at runtime, as if out of thin air. To generate the request actions Blanket defines a small private "macro" (heavy quotes around that as Ruby does not really support macros, this just makes it look like one):
request method is where the actual HTTP request happens, so this macro is just a way to conveniently add all of the similar request actions at runtime. Here’s how all the relevant methods are added:
Classes can be dynamic too!
While developing Exception raising support, I came across a fun problem: I wanted to expose specific
Blanket::Exception classes for well known HTTP status codes such as
500, you get the drift. This would make capturing and handling different types of exceptions easier.
One could surely just type all this code in, but there are about 60 well-known status codes and I don’t really enjoy repetitive tasks that much. Let’s take a map of status codes, using the code as a key and the value as it’s meaning in a human readable format:
Let’s get on to the fun part: generating
Blanket::Exception subclasses for each of these!
There are a few things happening here. For each code/message pair, we’re creating a new Class inheriting from
Exception, defining a method in it, setting it as a constant and finally, adding it to another map,
EXCEPTIONS_MAP. This last step allows to raise a new 500 Exception like this:
Because we also defined each of these exceptions as a constant, you could also do this directly:
Documenting your endeavours
Writing code that writes code can obviously be very powerful, but software cannot be written solely for the computer to understand. Because your code will, at some point, probably be used by other human beings, documenting how it works is of paramount importance. But how do you document that which is not there?
YARD is a documentation tool for Ruby projects. Here’s how you may generally document a static method:
add_action "macro" we talked about earlier, used to define the various RESTful action methods? We can use YARD’s
@macro directive to dynamically create documentation for all invocations of
When we call
:get, for example, documentation would be created for a public method
get, with the text Performs a get request on the wrapped URL as well as all available parameters and return types.
Wrapping it all up
Metaprogramming allows you to build wonderfully expressive interfaces while writing as little code as possible. In the case of Blanket, the whole project is comprised of three classes weighing around 40–90 LOC each, including documentation. For such a little utility, I would argue it packs a whole lot of functionality and delivers in a very terse and declarative way.
While it can be a powerful tool, in most cases you should exercise caution and restraint when using it, as its misuse can quickly muddle a codebase and lead to confused and frustrated collaborators trying to make sense of your highly abstracted code.
Header Photo by Elvis Pucar