arrow_backward Back to blog

Implementing DRY Magic Methods in Ruby

As a new developer to Ruby you might wonder how certain methods seem to be magically available without being strictly defined. Rails's dynamic finders (e.g. find_by_name) are one example of this kind of magic. It's very simple to implement magic such as this in Ruby, but it's also easy to implement things in a way that doesn't entirely mesh with standard Ruby object expectations.

Your Friend method_missing

The way that many magic methods are implemented is by overriding method_missing. This special method in Ruby is automatically called by the interpreter whenever a method is called that cannot be found. The default behavior of method_missing is to raise a NoMethodError letting the user know that the method that was called does not exist. However, by overriding this behavior we can allow the user to call methods that aren't strictly defined but rather programatically determined at runtime. Let's look at a simple example:

     class Nullifier       def method_missing(*args)         nil       end     end      nullifier = Nullifier.new     nullifier.some_method     # => nil     nullifier.foo(:bar, :baz) # => nil 

Here we simply told method_missing to immediately return nil, regardless of the method name or arguments passed. This essentially means that, for this class, any method call that is not defined on Object (the default superclass for new classes) will return nil.

While this example is certainly interesting, it doesn't necessarily give us more use in the real world. Let's take another example that actually does something useful. Let's make a hash that allows us to access its keys by making method calls:

     class SuperHash < Hash       def method_missing(method_name, *args)         if key?(method_name.to_s)           self[method_name].to_s         else           super         end       end     end      h = SuperHash.new     h['abc'] = 'def'     h.abc            # => 'def'     h.something_else # => NoMethodError 

This behavior gives us something pretty simple yet powerful: we have manipulated the foundation of the class to give us runtime methods. There's a problem, though: using method_missing alone is only half the story.

Quack Check With respond_to?

In Ruby, you can call respond_to? with a symbol method name on any object and it should tell you whether or not that method exists on the object in question. This is part of what makes Ruby's duck-typing work so well. So in our example, we also want to be able to know if a method is there using respond_to?. So let's add a new override for the respond_to? method of our example above:

     class SuperHash < Hash       def respond_to?(symbol, include_private=false)         return true if key?(symbol.to_s)         super       end     end 

Well, that was easy enough. Now our SuperHash will return hash keys based on method_missing and even tell you if the method is there with respond_to?. But there's still one more thing we can do to clean things up a bit: notice how we have repeated functionality in that we check key? in both methods? Now that we have a respond_to? we can use that as a guard for method_missing to make it more confident:

     class SuperHash < Hash       def method_missing(method_name, *args)         return super unless respond_to?(method_name)         self[method_name].to_s       end     end 

Wait, that can't be right, can it? Can we just assume that we can call the key like that? Of course! We already know that no existing method was called if method_missing is activated. That means that if respond_to? is true but no existing method was called, there must be a key in our hash that caused respond_to? to return true. Therefore we can confidently assume that the key exists and simply return it, removing the conditional and cleaning up the method_missing substantially.

Now that you know how method_missing and respond_to? can work together to add functionality to an object at runtime, you have a powerful new tool in your metaprogramming arsenal. Enjoy it!

arrow_backBack

New Project Request