coderrr

July 11, 2008

Solving the method_missing/respond_to? problem

Filed under: ruby — Tags: — coderrr @ 1:08 pm

Whenever you define method_missing to accomplish some metatrickery you need to remember to also redefine respond_to? so that it reflects the same new logic which you’ve added to method_missing. This is annoying to do and people don’t always do it. For example, Rails dynamic finders will not respond_to? properly (until after they’re called for the first time).

I thought up a little way to get both the method_missing logic and respond_to? logic working without any duplication needed. We define a new method, method_missing_proc, which instead of actually running code returns a lambda with the code that should be run. If no code should be run, nil is returned. With this, respond_to? can call method_missing_proc and if the return result is non-nil it knows to return true. method_missing can call method_missing_proc and then call the lambda if one is returned otherwise calling the original method_missing method to allow the method lookup chain to continue.

Update: I decided to include an implementation for the method method as well. I was talking to someone who had a legitimate-ish use case for having it. Now when someone calls object.method(:something), if method_missing_proc returns a lambda for that method name, an actual method will be created on the singleton class of the object and a Method object representing it returned.

Here’s the implementation:

class Object
  alias_method :old_method_missing, :method_missing
  def method_missing m, *a, &b
    l = method_missing_proc m, *a, &b
    return l.call  if l

    old_method_missing m, *a, &b
  end

  alias_method :old_respond_to?, :respond_to?
  def respond_to? m
    old_respond_to?(m) || !!method_missing_proc(m.to_sym)
  end

 alias_method :old_method, :method
  def method m
    old_method m
  rescue NameError
    l = method_missing_proc m
    (class<<self;self;end).class_eval { define_method m, &l }
    retry
  end

  def method_missing_proc *a; nil; end
end

And here’s an example of it in use:

class X
  def method_missing_proc m, *a, &b
    case m.to_s
    when /^hello_(\w+)$/
      lambda { puts "hello #{$1.gsub('_',' ')}" }
    when /^say_(\w+)_to$/
      lambda do
        raise ArgumentError, "You must specify who to say #$1 to"  if a.empty?
        puts "#{a.first}: #$1"
      end
    else
      super
    end
  end
end

x = X.new
p x.respond_to?(:hello_my_friend)  # => true
p x.respond_to?(:hello_world)         # => true
p x.respond_to?(:say_hi_to)            # => true
p x.respond_to?(:goodbye_world)    # => false

x.hello_world                          # => hello world
x.say_hi_to :sam                     # => sam: hi
x.say_hi_to rescue p $!             # => #<ArgumentError: You must specify who to say hi to>
x.goodbye_world rescue p $!      # => #<NoMethodError: undefined method `goodbye_world' for #<X:0xb7c576c0>>
# this time the method will actually be generated
p x.method(:hello_my_good_friend) # => #<Method: #<X:0xb7a93064>.hello_my_good_friend>
# this time the method will already exist
p x.method(:hello_my_good_friend) # => #<Method: #<X:0xb7a93064>.hello_my_good_friend>

Right after writing this I found that Mark Hubbart had already come up with the same solution over 3 years ago in the ruby-talk mailing list. His implementation is very similar, I swear I didn’t steal it! He actually went a step further and overrided the ‘method’ method to return the lambda from method_missing_proc. I’m not so sure if this is a good idea since you wouldn’t be getting back an actual Method object. Although you could modify the code to create a temporary method and return a Method object pointing to that. I’m not sure how important overriding ‘method’ is anyway.

So does anyone think this method_missing_proc technique is actually useful? Or just solving a problem which doesn’t exist?

6 Comments »

  1. Very useful. I have implemented method_missing on several occasions but rarely do it completely enough to cover respond_to? (or even method).

    Your strategy would allow me to have a more complete implementation without any more work. Always a win. I would think this would be something nice to implement in a support library like ActiveSupport or Facets. This way I would have to think about it. Just implement method_missing_proc and done.

    Comment by Eric Anderson — July 11, 2008 @ 2:30 pm

  2. The ActiveRecord dynamic finder problem that you mention was fixed in my patch – http://dev.rubyonrails.org/ticket/11538.

    Comment by Floehopper — July 13, 2008 @ 1:19 pm

  3. hey thats pretty cool

    Comment by hellobennguyen — July 14, 2008 @ 1:03 pm

  4. [...] might want to be aware of. If a class implements methods through a method_missing method then try might not recognize a valid method call because respond_to? will return false, and therefor try will return nil instead of the value that [...]

    Pingback by Try() as you might | Lambda @ Copa — January 11, 2009 @ 12:43 pm

  5. Very nice and almost totally removes my objections against (the overuse of) method_missing. One should be able to ask the duck if it walks like one without having to make it actually walk.

    Comment by Jonas Elfström — October 9, 2009 @ 6:32 pm

  6. pasted from my skype discussion about this comment thread.

    [22:03:24] Marcos Toledo: the fact is that you can’t really know if the duck walks like a duck unless it can walk without parameters
    [22:04:09] Marcos Toledo: you can only know if it can or cannot walk. and to me, asking how it walks without making it walk is probably wrong

    Comment by mtoledo — October 9, 2009 @ 8:07 pm


RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.