FiveRuns Blog

On Rails production performance and monitoring

Posts
3 comments

The Joy of Class.new

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.new is 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_s so 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!

Bookmark and Share
Continued Discussion

3 responses to this entry

http://pastie.textmate.org/158618

Ryan Carmelo Briones Ryan Carmelo Briones said:

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_set is great fun. I’m forward to the day when its good friend, const_missing, is appropriate.

Adam Keys Adam Keys said:

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. ;-)

Sam Smoot Sam Smoot said:

on March 15, 2008 at 12:06 PM

Contribute

Continue the conversation and share your thoughts. A name is required. Your e-mail address will not be displayed on the site. Textile formatting may be used in your comments (but will not be rendered in the live comment preview).

→ Posted by You on June 28, 2009 at 02:22 PM

Flickr

FiveRuns tagged photos on Flickr.

  • dwi_1106e
  • Five Runs, Eric Schank, Lauren Sell and Brian Gugliemetti
  • fiveruns-penn-and-teller-10
  • Bruce Williams
  • dwi_1106f
  • fiveruns-penn-and-teller-11
  • fiveruns-penn-and-teller-4
  • Eric Lindvall and Penn

See more FiveRuns tagged photos…

Previous Entries

Other Categories

Entries are also organized under the following general topic categories.