Really really simple Ruby metaprogramming
Metaprogramming in Ruby has the reputation of being something only the true zen Ruby masters can even hope to understand. They say the only true way to learn metaprogramming is to train with Dave Thomas in his mountain retreat for five years - the Ruby equivalent of this XKCD:
But it’s not that bad. In fact, I’m going to go so far to say that it’s possible to learn to metaprogram in Ruby without even meeting Dave Thomas. Yeah, I know, it doesn’t sound possible. But just you wait…
What is metaprogramming?
Metaprogramming is what makes Ruby awesome. It’s writing code to write code. It’s dynamic code generation. It’s the move from imperative to declarative. It’s a rejection of Java’s endless repetition of boilerplate code. It’s the living embodiment of DRY. Here’s an example:
class Monkey
def name
@name
end
def name= n
@name = n
end
end
I can see you there, in the middle of the classroom, with one arm straining up, the other one holding it up because it’s so damn hard to hold up. OK, Jimmy, what is it? Oh, you don’t need to write all that code in Ruby? You can just use attr_accessor
?
Jimmy’s right. That code snippet above could be written like so:
class Monkey
attr_accessor :name
end
So, attr_accessor
’s magic right? Well, actually, it’s not. Just like in Scooby-Doo where what looked like magic to start off with turned out to be an elaborate hoax, attr_accessor
is just a class method of Module
. It defines the name
and name=
methods on Monkey
, as if you’d manually defined them.
And that’s all metaprogramming is. Just a way of adding arbitrary code to your application without having to write (or copy-paste) that code.
An (extended) example
The source code for this example is available in my StringifyStuff plugin on github, which in turn was adapted from Ryan Bates’ Railscast Making a Plugin. Incidentally, if you feel you can improve the plugin, fork me on github!
I’m assuming basic knowledge of Ruby, and some familiarity with Rails would be helpful as well, but not essential.
Have a quick peek at the github repo now - the important files to look at for now are init.rb and lib/stringify_time.
First, stringify_time. This file defines a module called StringifyTime
, which defines a method called stringify_time
:
module StringifyTime
def stringify_time *names
#crazy stuff going on in here
end
end
The other two files, stringify_stuff and stringify_money are similar.
Now, init.rb:
class ActiveRecord::Base
extend StringifyStuff
extend StringifyTime
extend StringifyMoney
end
This extends ActiveRecord::Base
with the three modules listed. This now means that any class that inherits from ActiveRecord::Base
(e.g. your models) now has the methods defined in each of those modules as class methods.
The StringifyStuff
plugin is used like so:
class Project < ActiveRecord::Base
stringify_stuff
stringify_time :start_date, :end_date
end
stringify_time
is passed a list of symbols representing the relevant model attributes. It will provide useful accessors for those attributes. Let’s have a look at a simplified version of stringify_time
:
module StringifyTime
def stringify_time *names
names.each do |name|
define_method "#{name}_string" do
read_attribute(name) &&
read_attribute(name).strftime("%e %b %Y").strip()
end
end
end
end
stringify_time
is passed a list of symbols. It iterates over these symbols, and for each one calls define_method
. define_method
is the first clever Ruby metaprogramming trick we’re going to look at. It takes a method name and a block representing the method arguments and body, and magically adds an instance method with that name, arguments and body to the class in which it was called. In this case, it was called from Project
, so this will gain a new method with the name "#{name}_string"
. In this example, we passed in :start_date
and :end_date
, so two methods will be added to Project
: start_date_string
and end_date_string
.
So far so good. Now though, it gets a little bit hairy, so hold onto your horses (or equivalent equid).
In a normal model instance method, you’d access an attribute using a method of the same name. So if you wanted to get the start_date
converted to a string, you’d write:
def my_method
start_date.to_s
end
The problem with doing this in define_method
is that we don’t have start_date
as an identifier - it’s held as a symbol. There are two ways to accomplish the above if start_date
was passed in as a symbol:
def my_method attr_sym
read_attribute(attr_sym).to_s #This is a Rails method
end
or:
def my_method attr_sym
send(attr_sym).to_s #Ruby built-in method
end
For simplicity, I’m using write_attribute
, but send
is useful to know about too.
So, back to define_method
:
define_method "#{name}_string" do
read_attribute(name) &&
read_attribute(name).strftime("%e %b %Y").strip()
end
So, each time round the loop, name
will be one of the symbols passed into stringify_time
. Let’s go with start_date
to see what happens. define_method
will define a new instance method on the Project
class called start_date_string
. This method will check to see if there is a non-nil attribute called start_date
, and if so, call strftime
on it. This formatted date will be the return value of the method.
Wrap up
That’s more than enough for one day (I’ve been told off for writing kilo-word posts before). I’ll explain the workings of the rest of the plugin in a future post.
Metaprogramming is writing code that gives you more code. It’s a bit like the Sorcerer’s Apprentice; in fact, I think that should be recommended watching for this subject.
Be the Sorcerer. Don’t be Mickey.