back

How delegate works in Rails, Part II

This is the second part of a quick look at the delegate method in Rails.

Start with Part I here.

Here’s the code:

In part I, we saw that the delegate method first checks to make sure that options are passed in as a hash, and that if we are including prefix as an option, that we are using a method, not a class variable or constant.

So, we’re here:

    prefix = options[:prefix] && "#{options[:prefix] == true ? to : options[:prefix]}_"

This line sets the variable prefix to either nothing, to the thing being delegated to, or to the explicitly defined prefix argument.

For example, if we called delegate with the options

  delegate :name, :address, :to => :client, :prefix => true

In this case, prefix would equal client, because

>> prefix = true && "#{true == true ? :client : true}"
=> "client"

This is a cool way of settings strings that the method uses. Here are two examples to show how it works:

>> prefix = true && "foo"
=> "foo"
>> prefix = "hi" && "foo"
=> "foo"

Basically, prefix will only point to a string if x is true in x && y, so if options[:prefix] is false, then prefix will also be false.

Now that the method has validated the arguments and defined what the prefix should be, it’s time to do the actual delegating:

  methods.each do |method|
      module_eval(<<-EOS, "(__DELEGATION__)", 1)
        def #{prefix}#{method}(*args, &block)
          #{to}.__send__(#{method.inspect}, *args, &block)
        end
      EOS
  end

So, we’re looping through all of the methods that we want to delegate, and defining a method for each of them. Obviously, the method that we define just calls the method that we’re delegating on the target object. Powerful and simple.

So, in the above example, delegate would just define client_address, which would then call address on the client object.

Delegate does this using module_eval, which takes up to three arguments.

The first argument is a string, and the second two provide information for error messages. The __DELEGATION__ being passed in looks like it could be almost a keyword literal, but as far as I can tell is just a string. I’m not sure exactly why the code uses “(__DELEGATION__).” However, similar things appear to be used in other places in ruby/rails as well.

The final optional argument to module_eval is the line number that is should start at for errors. For example, if an error occurred then rails would raise (__DELEGATION__):X: where X is the lines below module eval.

Moving along

So, since the first argument to module eval is a string, we’re passing it <<-EOS instead of a huge string inside the parens. The arrow arrow dash stuff is just a way to define a multi line strong. The string ends when we pass in EOS. For more, check out Jay Fields. We could also just say

module_eval("def foo; puts 'hi'; end;", "(__DELEGATION__)", 1)

But that would get pretty ugly for longer methods.

So it’s kind of complicated, but the entire string that we pass in to module eval basically gets sent to Matz via Japanese mail (which is much faster) and he hacks into our computer and puts everything in that string inside of whatever class we called it from.

The cool part is that before he puts the string in he evaluates the stuff inside each #{}.

So, if we were doing something like delegate :foo, :to => :bar

   def #{prefix}#{method}(*args, &block)
    #{to}.__send__(#{method.inspect}, *args, &block)
   end

becomes,

   def foo(*args, &block)
    bar.__send__(":foo", *args, &block)
   end

__send__ is just another way of calling send, in case someone has defined self.send for some class. Also note that :symbol.inspect returns “:symbol”

Now our method is defined, since delegate is called when the class is loaded, so the delegated method will be available for all of our objects of that class.

(Please let me know if I have anything wrong, or if this was / wasn’t useful)

December 08, 2008