Metaprogramming Ruby, Pt. 1: Markaby
With the recent beta release (books can be beta now?) of Metaprogramming Ruby by Paolo Perrotta, I figured I’d share some of my personal knowledge and tricks in the field of meta-programming. Hopefully I can make this a multi-part series of articles on the subject.
In this first article I’m going to implement a standard Markaby-style DSL that takes declarative Ruby method calls and translates them into HTML. The example only really highlights one metaprogramming trick, namely passing a block to instance_eval
to have code executed in the context of an arbitrary object. The rest is just fluff, so it should be simple enough to follow.
Markamini, Our Example DSL
So we want to make a simple DSL where whichever declarative method we call inside a block will be turned into an HTML-like tag in our output, much the way Markaby works. We’ll call this “Markamini”. For example, we should be able to write the following:
Note: I use Ruby 1.9 syntax in all of my examples.
html = Markamini.document do
html do
head do
meta name: 'charset', content: 'utf-8'
title 'hi'
end
body do
div(id: 'foo') { "A div block" }
end
end
end
puts html
And get the HTML:
<html>
<head>
<meta name="charset" content="utf-8" />
<title>hi</title>
</head>
<body>
<div id="foo">A div block</div>
</body>
</html>
The Implementation
You can probably guess that method_missing
(the catch-all method call in Ruby) will be involved here in order to handle our arbitrary method calls. The question is, where should this method_missing
be defined? From the example above, it looks like we’re actually executing the methods html
, head
, etc. in the same context as Markamini.document
. Do you think we’d define method_missing
globally? That would be ugly, wouldn’t it. You’ll be glad to know we’re not going to do that. Instead, we use instance_eval
and blocks to trick Ruby into running this code in a localized context. This is where the magic happens.
Blocks are Procs, Procs are Objects, and Objects get passed around.
Remember that blocks are basically Proc’s, which means we don’t need to immediately execute their contents and can store these blocks for later or pass them around as the Proc’s they are. In this case we don’t have to store them for long, but we are going to avoid yield
ing to these blocks and instead use them as Proc objects. In fact, we’re going to pass the blocks (now a Proc) to another object for it to execute through instance_eval
.
The Trick: instance_eval(&block)
That’s the first (and only) trick here. The block passed to Markamini.document
is not yielded, but rather passed to Markamini itself to be executed in the context of a localized object. How do we do this? It’s simple:
def self.document(&block)
new.instance_eval(&block).to_s
end
We pass the block Proc object as a block (we use the &
prefix to tell Ruby that the argument is not an argument but an actual block) to instance_eval
to have it run our code as if it was running inside an instance of Markamini. We could run this on an instance of any object, but since we’re already in that Markamini class we might as well use it. Now that our code is being eval’d as if it was run inside Markamini, our methods are being sent to the instance, so let’s define our method_missing
:
def method_missing(name, opts = {}, &block)
parent = @node
@node = Node.new(parent, name, opts)
parent.children << @node if parent
if block_given?
val = instance_eval(&block)
@node.children << val if val.is_a?(String)
end
parent ? @node = parent : @node
end
Most of this is fluff to create our doubly-linked list of Node
objects to represent our tree of HTML elements. The one important line is the instance_eval(&block)
once again in the middle. We take all inner blocks and send them all back to our Markamini instance, so methods are always being sent to the same method_missing
.
The Implementation Fluff
So that’s the basics of implementing a Markaby-like DSL, but for completeness I’ll list the full source including the Node
class:
class Markamini
attr_accessor :node
def self.document(&block)
new.instance_eval(&block).to_s
end
def method_missing(name, opts = {}, &block)
parent = @node
@node = Node.new(parent, name, opts)
parent.children << @node if parent
if block_given?
val = instance_eval(&block)
@node.children << val if val.is_a?(String)
end
parent ? @node = parent : @node
end
end
class Node
attr_accessor :name, :options, :children, :parent
def initialize(parent, name, options = {})
@options = options
@name = name
@parent = parent
@children = []
if options.is_a?(String)
@options = nil
@children = [options]
end
end
def to_s
if children.size > 0
"<#{name}#{' ' + attrs unless attrs.empty?}>#{children.join}</#{name}>"
else
"<#{name}#{' ' + attrs unless attrs.empty?} />"
end
end
def attrs
return "" if options.nil?
options.map {|k, v| "#{k}=#{v.inspect}" }.join(" ")
end
end
As mentioned above, our Node class implements a standard HTML element in a doubly-linked list (with a parent and child nodes). We then collapse the root node with #to_s
which is called on all children nodes. We also setup attributes or check to see if we’re passed a String instead of attributes, or maybe our block yields a String value instead of elements. Standard HTML-DSL stuff.
What We’ve Learned
The important part is that by now you hopefully understand how to use instance_eval
and execute a block of code in a certain context. This is the underlying fundamental concept behind every DSL out there, and should get you started writing your own Ruby DSLs.