Tuesday, March 1, 2011

method_missing gotchas in Ruby

Are there any things to be careful about when defining the method_missing method in Ruby? I'm wondering whether there are some not-so-obvious interactions from inheritance, exception throwing, performance, or anything else.

From stackoverflow
  • A somewhat obvious one: always redefine respond_to? if you redefine method_missing. If method_missing(:sym) works, respond_to?(:sym) should always return true. There are many libraries that rely on this.

    Later:

    An example:

    # Wrap a Foo; don't expose the internal guts.
    # Pass any method that starts with 'a' on to the
    # Foo.
    class FooWrapper
      def initialize(foo)
        @foo = foo
      end
      def some_method_that_doesnt_start_with_a
        'bar'
      end
      def a_method_that_does_start_with_a
        'baz'
      end
      def respond_to?(sym)
        pass_sym_to_foo?(sym) || super(sym)
      end
      def method_missing(sym, *args, &block)
        return foo.call(sym, *args, &block) if pass_sym_to_foo?(sym)
        super(sym, *args, &block)
      end
      private
      def pass_sym_to_foo?(sym)
        sym.to_s =~ /^a/ && @foo.respond_to?(sym)
      end
    end
    
    class Foo
      def argh
        'argh'
      end
      def blech
        'blech'
      end
    end
    
    w = FooWrapper.new(Foo.new)
    
    w.respond_to?(:some_method_that_doesnt_start_with_a)
    # => true
    w.some_method_that_doesnt_start_with_a
    # => 'bar'
    
    w.respond_to?(:a_method_that_does_start_with_a)
    # => true
    w.a_method_that_does_start_with_a
    # => 'baz'
    
    w.respond_to?(:argh)
    # => true
    w.argh
    # => 'argh'
    
    w.respond_to?(:blech)
    # => false
    w.blech
    # NoMethodError
    
    w.respond_to?(:glem!)
    # => false
    w.glem!
    # NoMethodError
    
    w.respond_to?(:apples?)
    w.apples?
    # NoMethodError
    
    Christoph Schiessl : That's interesting. How would you implement that for a class which consists of "normal" methods and "dynamic" methods (implemented via method_missing)?
    John Feminella : @Christoph: Your `pass_sym_to_foo?` method becomes a generic `handle?` method which decides whether to try to process this request or hand it off to `super`'s `method_missing`.
  • If you can anticipate method names, it is better to dynamically declare them than to rely on method_missing because method_missing incurs a performance penalty. For example, suppose you wanted to extend a database handle to be able to access database views with this syntax:

    selected_view_rows = @dbh.viewname( :column => value, ... )
    

    Rather than relying on method_missing on the database handle and dispatching the method name to the database as the name of a view, you could determine all the views in the database ahead of time, then iterate over them to create "viewname" methods on @dbh.

  • Building on Pistos's point: method_missing is at least an order of magnitude slower than regular method calling on all the Ruby implementations I've tried. He is right to anticipate when possible to avoid calls to method_missing.

    If you're feeling adventurous, check out Ruby's little-known Delegator class.

0 comments:

Post a Comment