Getting started with Ruby metaprogramming

The Rails auto_complete plugin was my first exposure to Ruby metaprogramming. It’s code was simple enough for a Rails beginner like me to understand, but also just complex enough for me to learn something new. Specifically, I ran into metaprogramming when I took a close look at the “auto_complete_for” method and tried to figure out how it worked. I won’t spend any time here explaining what the auto_complete plugin does or what it’s used for, beyond to say that if you add this line to one of your controllers:


class CategoriesController < ApplicationController
  auto_complete_for :category, :name

This is very cool, and is a typical example of Ruby on Rails magic: you add one line to a class in your application and suddenly an entire feature or behavior is added, customized to the data and objects in your app!

This sort of thing is really what makes Ruby on Rails so amazing… but how does it work? Let’s take a look at the implementation of auto_complete_for method:

def auto_complete_for(object, method, options = {})
  define_method("auto_complete_for_#{object}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[object][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = object.to_s.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

You can find this code in the lib/auto_complete.rb file inside the rails/auto_complete repository on github. So what the heck does all of this mean? Let’s take a step-by-step look at this code, and see if we can figure it out.

To get started, let’s use the category/name example I used in my last article, and also that Ryan Bates used in his Auto-Complete Association screencast on the auto_complete plugin:

auto_complete_for :category, :name


Here we are passing “:category” into auto_complete_for as the value for “object,” and “:name” as the value for “method.” Now let’s repeat the auto_complete_for code, but substitute object with :category:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = :category.to_s.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

In the code snippet above I’ve highlighted the places where the symbol :category appears. You can see that it’s used in a few different places, but the most important line is near the bottom: @items = :category.to_s.camelize.constantize… etc. Let’s evaluate each of the method calls on this one line, one at a time. The first call is “:category.to_s”. The “to_s” method name means “to string” and will convert the target object (the object you call to_s on) to a string. This means that the :category symbol will be converted to a string. So now let’s display the string ‘category’ in place and see what we are left with:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = 'category'.camelize.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

You can see ‘category’ highlighted above where I’ve evaluated the to_s method. Now the next method call is “camelize” – what does this mean? The camelize method is one of a series of functions that Rails provides in the “ActiveSupport” gem, one of the components of the Rails framework. It converts the given string to camel case, for example “office_code” to “OfficeCode.” Since Ruby class names are written using camel case, this is often very useful for obtaining a class name from a string. In our example, the string “category” is converted into “Category” with an upper case “C” …

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = 'Category'.constantize.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Now let’s take a look at the next method call on that same line of code: “constantize.” This converts a string into an actual Ruby constant, and returns an error if that constant doesn’t exist. In our example, the string “Category” is converted into the Ruby class Category:

def auto_complete_for(:category, method, options = {})
  define_method("auto_complete_for_#{:category}_#{method}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{method}) LIKE ?",
                       '%' + params[:category][method].downcase + '%'
                     ],
      :order => "#{method} ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{method}' %>"
  end
end

Now we can see that the complex line above evaluates to a simple ActiveRecord find call. The power of Ruby metaprogramming has enabled us to write a single helper function “auto_complete_for” that takes any symbol or string as an argument (e.g. :category), and performs a SQL query on the corresponding ActiveRecord class. The amazing part of this for me is how flexible and easy to use Ruby is: helper methods like camelize and constantize make it very easy to extract common bits of code you might write over and over again in your app, and generalize them into a single method that will apply to any target class. This would be possible in any programming language but it’s amazing just how easy it is to do with Ruby.

Let’s continue to simplify the auto_complete_for code by substituting a value for the “method” parameter – in our example method will become the symbol “:name” :

def auto_complete_for(:category, :name, options = {})
  define_method("auto_complete_for_#{:category}_#{:name}") do
    find_options = { 
      :conditions => [
                       "LOWER(#{:name}) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "#{:name} ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, '#{:name}' %>"
  end
end

Again you can see that the :name symbol is used in a few different places. Most commonly here the symbol is inserted in a string using this syntax: “#{:name}”. This is the standard Ruby #{} string interpolation operator, which is also implicitly converting the symbol into a string before inserting it into the surrounding string value, just as if we called to_s as above. So let’s replace “#{:name}” and “#{:category}” with the strings “name” and “category,” and then also insert them into the surrounding string values:

def auto_complete_for(:category, :name, options = {})
  define_method("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }.merge!(options)

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

And now let’s replace the last parameter to auto_complete_for “options” with it’s default value since we aren’t providing that in our auto_complete_for :category, :name call:

def auto_complete_for(:category, :name, {})
  define_me thod("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }.merge!({})

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

You can see that options was used in only one place: …merge!(options). Using merge with hashes is another extremely common technique in Ruby coding; it adds the key/value pairs from the hash you provide as a parameter to the hash you call merge on. Merge! is a variation on this which directly modifies the target object, as opposed to only returning the merged hash. Merge is very useful for metaprogramming because, as in this example, it makes it easy to allow the user/client code of a method to customize a hash of options that is passed into some other function. Here merge was used to allow the caller of auto_complete_for to pass in additional find options to the Category.find call. In our case since we didn’t pass in a value for options, it is set to {} and then has no effect on the find_options hash. So let’s just remove the call to merge!({}), which is a NOP anyway:

def auto_complete_for(:category, :name, {})
  define_method("auto_complete_for_category_name") do
    find_options = { 
      :conditions => [
                       "LOWER(name) LIKE ?",
                       '%' + params[:category][:name].downcase + '%'
                     ],
      :order => "name ASC",
      :limit => 10 }

    @items = Category.find(:all, find_options)

    render :inline => "<%= auto_complete_result @items, 'name' %>"
  end
end

Now the code is looking simpler and simpler. Let’s finish this example by evaluating the “define_method” call at the top. As you might guess, define_method is how you can dynamically create a new method in a class in Ruby. In auto_complete_for it’s used to add a new method to the controller in which you included the call to auto_complete_for, called “auto_complete_for_category_name.” In other words, adding the auto_complete_for :category, :name line to the CategoriesController was equivalent to adding this method definition to the class:

def auto_complete_for_category_name
  find_options = { 
    :conditions => [
                     "LOWER(name) LIKE ?",
                     '%' + params[:category][:name].downcase + '%'
                   ],
    :order => "name ASC",
    :limit => 10 }

  @items = Category.find(:all, find_options)

  render :inline => "<%= auto_complete_result @items, 'name' %>"
end

Now we’ve seen the real value of metaprogramming: the author of the auto_complete plugin was able to provide a simple helper method, auto_complete_for, which dynamically added this fairly complex method to your controller. The beauty and power of Ruby and Rails is just how easy this was to do: the generated function is tailored to use the Category model just as if you had written it yourself. When I first came across this code I was impressed not only by the power that this sort of metaprogramming provided, but also by how easy it was to do this using Ruby on Rails. Helper modules and methods, such as constantize in ActiveSupport, make it very, very easy to convert from symbols to strings to constants and back again… which is exactly what you need to do to write a general method that can be dynamically generated in this way.

Last week I asserted that calling auto_complete_for in your controller like this was equivalent to this code from Ryan Bates’ Auto-Complete Association screencast:


class CategoriesController < ApplicationController

  def index
    @categories =
      Category.find(:all, :conditions => ['name LIKE ?', "%#{params[:search]}%"])
  end

etc...


Now you can see how this is true: the code Ryan wrote was a simple call to Category.find, with conditions that search for a name like the name provided in the “search” parameter. Looking at the simplified auto_complete_for_category_name method above, you can see that it calls Category.find in the same way. However, the auto_complete_for version is a bit different in that it:

  • Looks for a parameter called “category[name]” instead of “search,” and
  • Converts it to lower case, and
  • Passes a couple of other SQL options into Category.find: order by name ascending, and limit 10, and
  • Makes a call to render :inline to return the result to the browser without the need for a normal view code file.

Auto_complete_for does essentially the same thing that Ryan explained in his screen cast, but the elegance of Ruby metaprogramming allows the users of the auto_complete plugin to implement this search feature without writing any code at all.