An Intro to Ruby Metaprogramming

I've been reading the poignant guide of late. I remember giving it a go some time back but it didn't really click with me then. I'm not the biggest fan of reading technical books on hand-held electronic devices. Actually, it just seems to be a problem with the way codeblocks render on them. But the eBook on the iPad is alright as it's fairly concise. That's enough Apple endorsements already, on with the good stuff. I reach chapter six with its metaprogramming goodness and after reading it through a couple of times, I got the concept. But there were some lines of code that hadn't quite sunk in. So I decided to dig a little deeper and hopefully this post will provide an introduction to my learnings.

What is Metaprogramming and Why Use it?

There's a long winded (but no doubt accurate) metaprogramming definition on Wikipedia. Here's a much shorter, albeit Ruby specific one:

Ruby Metaprogramming is the writing of code that alters language constructs at runtime.

That's pretty short but it's not exactly plain English. What it's saying is, metaprogramming is writing code that writes code. That almost sounds like something from a science fiction movie. If that's not a good enough reason to start metaprogramming then perhaps this next one will be.

Metaprogramming results in writing less code.

You're already using it...

Even if you're relatively new to Ruby, you've probably been using the attr_accessor method. Take at look at this Beer class.

class Beer
  attr_accessor :name, :origin, :strength

def initialize(name, origin, strength) @name = name @origin = origin @strength = strength end end

Look familiar? Under the hood, there's some meta going on here. The attr_acessor method creates instance variables @name, @origin, and @strength. It also generates corresponding methods to read and write the attributes. As a result, our code base is kept DRY.

At runtime, our Beer class ends up looking something like this:

class Beer
  def initialize(name, origin, strength)
    @name = name
    @origin = origin
    @strength = strength
  end

def name= val @name = val end

def name @name end

def origin= val @origin = val end

def origin @origin end

def strength= val @origin = val end

def strength @origin end end

So we've ended up with 30 lines of code, but only written 9 of them. Awesome. Let's do some of our own meta then.

Beer Brewing

This example is probably somewhat contrived but the purpose here is to give you an idea of what's possible in Ruby. Kind of planting the seed as it were.

class Brewery
  # start the meta machine
  def self.ingredients(*arr)
    return nil if arr.empty?

<span class="n">arr</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
  <span class="c1"># friendly ingredient string</span>
  <span class="n">s</span> <span class="o">=</span> <span class="n">i</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[^a-z\s]/</span><span class="p">,</span> <span class="s1">''</span><span class="p">)</span>

  <span class="n">define_method</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">?"</span><span class="p">)</span> <span class="k">do</span>
    <span class="vi">@stock</span> <span class="o">||=</span> <span class="p">{}</span>
    <span class="c1"># check &amp; print stock level</span>
    <span class="k">if</span> <span class="vi">@stock</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
      <span class="s2">"We've got </span><span class="si">#{</span><span class="vi">@stock</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="si">}</span><span class="s2"> units of </span><span class="si">#{</span><span class="n">s</span><span class="si">}</span><span class="s2"> in stock."</span>
    <span class="k">else</span>
      <span class="s2">"We're out of </span><span class="si">#{</span><span class="n">s</span><span class="si">}</span><span class="s2">!"</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="n">define_method</span><span class="p">(</span><span class="s2">"add_</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">val</span><span class="o">|</span>
    <span class="vi">@stock</span> <span class="o">||=</span> <span class="p">{}</span>
    <span class="c1"># add ingredient to stock</span>
    <span class="vi">@stock</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span>
  <span class="k">end</span>
<span class="k">end</span>

end end

class BrewDog < Brewery ingredients :water, :malt, :hops, :yeast end

Hopefully our brewery explains itself but just in case, here's a breakdown of the meta part.

The BrewDog class inherits from the Brewery class and calls Brewery's ingredients class method. This iterates over the ingredients, and for each one creates two instance methods using define_method. The first returns the current stock level for that ingredient. The second adds stock for it.

b = BrewDog.new   #=> #<BrewDog:0x007fafdcb83308>
b.hops?           #=> "We're out of hops!"
b.add_hops 50     #=> 50
b.hops?           #=> "We've got 50 units of hops in stock."

As well as writing less code, this method also provides some flexibility. For example, what if our next brewery wanted to stock an ingredient other than water, malt, hops or yeast? No problem:

class FostersGroup < Brewery
  ingredients :tap_water, :cheap_malt, :mouldy_hops, :industrial_yeast
end

f = FostersGroup.new #=> #<FostersGroup:0x007f83b31e1d90> f.add_mouldy_hops 80 #=> 80 f.mouldy_hops? #=> "We've got 80 units of mouldy hops in stock."

Luckily for the brewers of that well known Australian brand larger, we can accommodate their inferior product and brewing practices. Perhaps in this case meta's not so great ;-]

Beer Drinking

I've barely scratched the surface with what's possible but hopefully this has been a good starting point. The rabbit hole goes a whole lot deeper once we start looking at metaclasses and Ruby's object model, but perhaps I'll leave that for another post.

The following resources are very much worth checking out.

And for the beer connoisseurs.

Moz Morris

Moz Morris

Freelance Web Developer