It doesn’t take much time in the Ruby world to hear the term duck typing. It wasn’t until I was writing a method for a recent project that was to accept either one or many ids that I actually stopped and actually investigated it. I knew what most people know about it: “If it walks like a duck and quacks like a duck, then it’s a duck” and that in actual practice this means you call call respond_to? to see if a method exists.
But as I was working on the code I found I kept wanting to type object.kind_of?(Array) but since this seemed to clash with the idea of duck typing I thought it was time to investigate a little bit.
The reason the type of an object using #kind_of? is not that useful is that Ruby supports multiple inheritance via the inclusion of modules. Since an object’s methods may have come from either a parent class or an included module, knowing the objects class doesn’t provide much information. If Tiger.new.kind_of?(Felidae) just returned true, is it safe to call the #domesticated? method? Calling #kind_of? just doesn’t help you make that decision.
module Wild
def domesticated?
false
end
end
class Felidae
def claws?
true
end
end
class Tiger < Felidae
include Wild
end
It seems that there are two schools of thought on duck typing. First, “soft” duck typing. Paraphrasing an example from Dave Thomas’ pickaxe book shows the standard approach:
def some_method param
if param.respond_to? :<<
# must be something we can write to!
param << "a string to be added"
end
end
The difficulty here is that respond_to? famously fails when you are using method_missing to catch and respond to method calls. The only way to be sure that respond_to? is not deceiving you is to go ahead and call the method. It is this approach that people call “hard” duck typing:
def some_method param param << "a string to be added" end
As Dave explains:
“You don’t need to check the type of the arguments. If they support << (in the case of result) or title and artist (in the case of song), everything will just work. If they don’t, your method will throw an exception anyway (just as it would have done if you’d checked the types). But without the check, your method is suddenly a lot more flexible: you could pass it an array, a string, a file, or any other object that appends using <<, and it would just work.”
— Programming Ruby second edition pg 355 – Dave Thomas
The interesting thing to me here, is the rationale for just calling the method instead of doing something like this:
def some_method param
begin
param << "a string to be added"
rescue NoMethodError => e
raise ArgumentError, "Needs to support the << method"
end
end
Constantly wrapping method calls in begin/rescue/end is pretty heinous from any angle; aesthetics, readability or performance. While hard duck-typing might come off as a little cavalier, it’s pretty much assumed in the Ruby community you have a test suite to make sure none of these NoMethod exceptions end up in production.
Another interesting take on the issue is the approach taken by Jeremy Kemper, the top commiter to the Rails framework. With commit 609c1988d2e274b he added the #acts_like? method to Object:
class Object
# A duck-type assistant method. For example, Active Support extends Date
# to define an acts_like_date? method, and extends Time to define
# acts_like_time?. As a result, we can do "x.acts_like?(:time)" and
# "x.acts_like?(:date)" to do duck-type-safe comparisons, since classes that
# we want to act like Time simply need to define an acts_like_time? method.
def acts_like?(duck)
respond_to? :"acts_like_#{duck}?"
end
end
and then to each of the core classes in the standard library he added an acts_like_x? method. Here is what the one for Date looks like:
class Date
# Duck-types as a Date-like class. See Object#acts_like?.
def acts_like_date?
true
end
end
These were added to ActiveSupport back in 2009 but for some reason they don’t seem to be widely used in the codebase. This surprises me because this approach seems to avoid NoMethodErrors without cluttering your code with rescue statements. On top of that it has none of the problems inherent in respond_to?.
I hope that helps people out a little. I know it has helped clarify my thoughts. Just one more reason to go and read the source.