Ruby 101: method_missing gotchas

Forgetting ‘super’ with ‘method_missing’

method_missing is a hallmark of Ruby metaprogramming. It is one of those coding techniques that you need to master if you want to move from white belt to black belt. Aside from that, it is also fun to use.

  class RadioActive
    def initialize(path)
      ...
    end

    def to_format(format)
      ...
    end
  end
  
  
  d = RadioActive.new('/path/to/uranium')
  d.to_format('xml')

It works but you want explicit methods for each format. So you tap into your Ruby skills and implement your own method_missing.

  class RadioActive
    def method_missing(name, *args)
      if name.to_s =~ /^to_(\w+)$/
        to_format($1)
      end
    end
  end
  
  
  d = RadioActive.new('/path/to/uranium')
  d.to_xml            # WORKS
  d.to_format('xml')  # WORKS
  d.undefined_method  # FAILS

Unfortunately, the last call to ‘undefined_method’ fails. Actually, you would not know it fails because Ruby will not fire any exception. In case there is an undefined method, let us see how Ruby handles it.

  >> s = 'uranium'
   => "uranium" 
  >> s.to_xml
  NoMethodError: undefined method `to_xml' for "uranium":String

There you go. But there is no need to raise the ‘NoMethodError’ in your code. Instead, simply call ‘super’ if you are not handling the method. Whether you have your own class or inheriting from another, do not forget to call ‘super’ with your ‘method_missing’

  class RadioActive
    def method_missing(name, *args)
      if name.to_s =~ /^to_(\w+)$/
        to_format($1)
      else
        super
      end
    end
  end
  
  d = RadioActive.new('/path/to/uranium')
  d.undefined_method
  # => in `method_missing': undefined method `undefined_method' for # (NoMethodError)

Calling ‘super’ is not just for ‘missing_method’. You also need to do the same for the other hook methods like ‘const_missing’, ‘append_features’, ‘method_added’.

Forgetting respond_to?

If you modify ‘method_missing’, it will also affect the behavior of ‘respond_to?’ because what you are adding in as methods do not actually exist — they are ghost methods. If you check the list of instance methods for our class, it will only show 2.

  >> RadioActive.instance_methods(false)
  => ["method_missing", "to_format"]
 
  >> d.respond_to?('to_format')
  => true
  >> d.respond_to?('to_xml')
  => false
  

Every time you modify ‘method_missing’, you also need to update ‘respond_to?’

  class RadioActive
    def respond_to?(name)
      !!(name.to_s =~ /^to_/ || super)
    end
  end
  

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s