Ruby has all sorts of great little methods. Pretty much everything in Enumerable is the bee’s knees. But today I want to regale you with the joy of a method we don’t often call – Class.new.
Creating new classes at runtime seems like a corner case, something you would rarely need to do. And it is. But when you do need it, you will thank your lucky stars that you can. So before I show you how to do it, let’s all thank our lucky stars.
I’ll wait.
Madlib’n
OK, now let’s get to it. What we’re going to do here is write a little gizmo to fill in Madlibs, though not quite like the Ruby Quiz version. We’re going to do use some gratuitous tricks along the way, so gird your loins.
The main text of our Madlib looks like this:
story = <<EOS
<%= @name %> said: "Now you just fought one hell of a <%= @animal %>
And I know you hate me, and you got the right
To <%= @verb %> me now, and I wouldn't blame you if you do.
But ya ought to thank me, before I die,
For the gravel in ya guts and the spit in ya eye
Cause I'm the <%= @adjective %> that named you "Sue.'"
I got all choked up and I threw down my <%= @thing %>
And I called him my pa, and he called me his son,
And I came away with a different point of view.
And I think about him, now and then,
Every time I try and every time I win,
And if I ever have a son, I think I'm gonna name him
<%= @name2 %> or <%= @name3 %>! Anything but Sue! I still hate that name!
EOS
The story is a familiar one, though I changed it up a little bit. And our Madlib will mutate it even further. Note that the values we’ll substitute are just ERB blocks.
Now, let’s look at the code that defines how we’ll fill in the Madlib and then print the result:
str = madlib(story) do
name 'Alistair'
animal 'duck'
verb 'tickle'
adjective 'fellow'
thing 'spatula'
name2 'Horace'
name3 'Cynthia'
end
puts str
Even if you’re relatively new to Ruby, this chunk of code probably looks familiar. We’ve defined a madlib that will use story as its template. We then specify the value to use for each blank in the Madlib.
So this API-of-sorts is really easy to use, but what makes it tick?
require 'erb'
def madlib(str, &block)
madlibber = Class.new
# Not yet...
end
m = madlibber.new(str)
m.instance_eval(&block)
m.to_s
end
Obviously, everything happens in madlib. We’ll dive into the glorious Class.new in a moment, but let’s just get our bearings here. We create the new class and assign it to madlibber. Remember, we can’t assign it to Madlibber because Ruby doesn’t allow assigning to constants inside method bodies.
Then we create a new instance of our madlib gizmo and we instance_eval the block we were passed (the one with name, animal, etc.) in madlibber. So basically, we’re running the code passed to us as though as it were in a method body inside madlibber. Neat!
Finally we convert our madlibber to a string by calling to_s. Now we have the madlib all filled out and ready for big laughs.
That’s the general idea. Let’s get to the details.
The Details
What lurks in that madlibber class?
madlibber = Class.new do
attr_reader :vars, :story
def initialize(story)
@vars = {}
@story = story
end
%w{name animal verb adjective thing name2 name3}.each do |var|
class_eval <<-EOR
def #{var}(val)
vars[:#{var}] = val
end
EOR
end
def to_s
vars.each_pair do |name, value|
instance_variable_set("@#{name}".to_sym, value)
end
ERB.new(story).result(binding)
end
end
Inside the block passed to Class.new we can do anything we normally do inside a class...end in Ruby. Heck, I could have written this whole thing as a standalone class, but then this article would have proven 100% extraneous and we can’t have that.
I’ve thrown in some idioms I see a lot in the FiveRuns code which I don’t see in most Ruby on Rails apps. However, I’ve seen it all over the place in the Rails code, so they’re handy idioms to wrap your head around.
We tend to throw an attr_reader on most instance variables. It saves a little finger typing (don’t have to type the @ all the time), sure. Mostly it looks a little nicer and makes it easier to substitute reading an instance variable with some actual logic down the road.
After the constructor there’s an array that defines all the blanks to fill in for the Madlib. We then iterate over that method and then define a method for each blank to set the appropriate value in our vars hash. So, once all that’s done, we’ve got the name, animal, etc. methods that are called in the block passed to madlib.
I’ve seen this trick a lot in Rails’ controller test bits. It’s handy to know about, as sometimes I’ve grep’d through some code looking for a method definition and missed it because the name was stuffed in an array.
Finally, in to_s, I iterate over each blank in the Madlib and set it as an instance variable. This was the quickest and easiest way I could think of to get each replacement string into the current binding so I could pass it to ERB. Once ERB is done, we’ve got our newly minted Madlib, ready for a hilarious read-through.
binding is a clever little bit of Ruby that gets you all the variables and instance variables of the context in which its called. I’ve only seen it used by ERB, but would love to hear about other clever ways to use it.
Wrapping up
So, there you go. That’s the joy of Class.new – create new classes at runtime, but there’s relatively little metaprogramming trickery to master. In the tradition of Ruby Quiz, I’m going to leave you with a few challenges:
- Right now the class generated by
Class.newis anonymous; printing it gives you something like<#<Class:0x79b6c>. Tweak the code so it gets a proper name. - Extract the anonymous class into its own class and fix up any problems that arise.
- DRY up the definition of the blanks. Right now they are specified in the anonymous class, the story template and then in the call to
madlib. That makes writing new Madlibs a real pain. - Change
to_sso it passes data into ERB without using instance variables.
Here’s the original source for your perusal and modification. Should you accept the challenge, leave the URL to your improvements in the comments!















Continued Discussion
3 responses to this entry
http://pastie.textmate.org/158618
on February 27, 2008 at 11:45 PM
Well done, Ryan. Kudos for using
__DATA__, that’s always a fun one to work in when its appropriate. Also,const_setis great fun. I’m forward to the day when its good friend,const_missing, is appropriate.on February 28, 2008 at 04:46 PM
Just to put it out there (not that anyone suggested it): Overwriting Object::const_missing is bad mojo. Sandbox that evil in your own module or class.
Maybe someday I’ll write a gem to detect adding const_missing directly to Object and raise a DontBeLazyError. ;-)
on March 15, 2008 at 12:06 PM