Develop => Deliver => Do it again

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
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.

1
2
3
4
>> 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’

1
2
3
4
5
6
7
8
9
10
11
12
13
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 #<RadioActive:0x1001696e8> (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.

1
2
3
4
5
6
7
>> 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?’

1
2
3
4
5
class RadioActive
  def respond_to?(name)
    !!(name.to_s =~ /^to_/ || super)
  end
end