Metaprogramming And Fate

Posted on Mar 30th, 2012
Categories: explaining, ruby

The other day someone asked me the Metaprogramming Motivation Question. The MMQ takes many forms, but it comes down to this: “Sure, sure”, the questioner says, “Ruby lets you do all of this cool metaprogramming stuff, but when would you ever want to use it?” I can’t remember exactly what my RubyNation answer was, but yesterday Fate reached down and smacked me on the head with a much better one.

The smack took the unlikely form of my buddy Dave Bock, who was lamenting on Twitter that some helpful @#$% had changed the data out from under one of his objects. As I understand it, the good Mr. Bock had a class that looked something like this:

class Book
  attr_reader :title

  def initialize(title)
    @title = title
  end
end

gpf = Book.new("The Girl Who Played With Fire")

Now to the casual observer (which Dave certainly is not, but we all make mistakes) the Book class appears to be about as immutable as anything in Ruby ever is – after all, there is that attr_reader big as life right up there at the top of the class.

What bit the good Dr. Bock was that when you say gpf.title you are getting back a reference to the original string, the one that the gpf object is hanging on to. Now here’s the punch line: Ruby strings are mutable. So all you have to do is something like this: gpf.title « ‘flies’ And now you have a girl who plays with fireflies, which is not the same thing at all. So are we Ruby programmers simply out of luck, doomed to forever be on our guard for those who might take the edge off of our trendy book titles?

No! We can just metaprogram our way out of this bind. To see how metaprogramming can help us, consider what that original attr_reader :title actually does: It creates a new method on the Book class, a method called title, a method which returns the value of the @title instance variable. The cool thing is that there is no special magic in attr_reader – we can write one of our own:

class Class
  def my_attr_reader(name)
    define_method(name) do
      instance_variable_name = "@#{name}".to_sym
      instance_variable_get(instance_variable_name)
    end
  end
end

Now it looks like there is a lot going on in the code above, but most of it is really just some unfamiliar syntax: We are simply adding a new method to an existing class. One of the confusing bits is that the existing class is the Class class, the base class of all Ruby classes (say that three times fast). The method we are adding is called my_attr_reader and it does something interesting: It defines a new instance method on the class that calls it. You pass in the name of the new method to my_attr_reader, perhaps :title and it will conjure that method up. And just what does that new title method do? Why it returns the value of the instance variable with the same name, in this case @title.

If you substitute my_attr_reader for the regular attr_reader method, like this:

class Book
  my_attr_reader :title

  def initialize(title)
    @title = title
  end
end

You will still end up with a class with a title method that does the right thing.

Which brings us back to the good Messrs Bock and Fate. Dave’s troubles all stem from the fact that when someone asks for title his objects are returning the one and only title string, leaving open the possibility of accidental modification. Now Dave’s woes would be over if only we had a variant of attr_reader, one which would return a copy or a clone of the original value. Hmmmmm:

class Class
  def safe_attr_reader(name)
    define_method(name) do
      instance_variable_name = "@#{name}".to_sym
      instance_variable_get(instance_variable_name).clone
    end
  end
end

Notice the call to clone right there at the end of the instance_variable_get method? If we switch the Book class over to using safe_attr_reader, like this:

class Book
  safe_attr_reader :title

  def initialize(title)
    @title = title
  end
end

Then we can do things like this all day long:

gpf.title << 'flies'
gpf.title.gsub!(/Girl/, 'Boy')
gpf.title.upcase!

Without making any changes to @title inside of gpf because the title method will always return a copy of the string. How’s that for some Metaprogramming motivation?

And so Fate, shaking his cowled head at the slowness of mere mortal thinking (and at the difficulties of getting a good hoagie in Northern Virginia) recedes into the darkness.

Russ

comments powered by Disqus