Walk Like a Duck

Introduction

In a duck-typed language like Ruby, it’s very important that you actually use duck typing. This is especially important when you’re designing a library or other code that could interact with objects that you don’t control. I have come across a few libraries lately that don’t follow duck typing conventions and have caused unexpected behavior when I’ve used them.

In this article, I’m going to pick on Shoulda. Thoughtbots, please do not take offense! I’m a fan, and I figured you guys could take it.

Duck Typing

it's a duck! Before I get into the details, I’ll say a word or two about duck typing for those who may need a refresher. The principle of duck typing basically says that if something “walks like a duck and talks like a duck” it probably is a duck. What this really means is that when you’re interacting with other objects, you should not care what they are, but rather how they behave. If you have an object that responds to #each, who cares if it’s an Array or Set or a custom collection?

Implementing Duck Typing

You should rarely ever use #is_a? on an object. You should be using #respond_to? instead. The whole point in duck typing is that you don’t care what kind of object you’ve received. The only thing you care about is that the object does what you want it to do. Let me show you an example from Shoulda.

The following code is from lib/shoulda/assertions.rb in Shoulda’s git repository. assert_contains is a custom assertion for Shoulda that allows you to easily test a collection for the presence of a particular element.

def assert_contains(collection, x, extra_msg = "")
  collection = [collection] unless collection.is_a?(Array)
  msg = "#{x.inspect} not found in #{collection.to_a.inspect} #{extra_msg}"
  case x
  when Regexp
    assert(collection.detect { |e| e =~ x }, msg)
  else
    assert(collection.include?(x), msg)
  end
end

If you look at the first line of the method definition, you’ll notice that they are calling is_a?(Array) on the “collection” this is passed into the function. The code I was writing was using a Set instead of an Array. The trouble is, if you pass anything other than an array, that line will wrap whatever you passed in an array. So I ended up with an Array with a Set inside of it, which caused the rest of the assertion code to fail.

The solution to this problem is very easy. If you look at my fork, all I’ve done is change that one line (and add some tests, of course). Now we only wrap the collection in an array if it doesn’t respond to include?. It’ll work with an Array still. It’ll work with a Set. It’ll work with your custom data structure that can detect the presence of objects. That’s the power of duck typing.

def assert_contains(collection, x, extra_msg = "")
  collection = [collection] unless collection.respond_to? :include?
  msg = "#{x.inspect} not found in #{collection.to_a.inspect} #{extra_msg}"
  case x
  when Regexp
    assert(collection.detect { |e| e =~ x }, msg)
  else
    assert(collection.include?(x), msg)
  end
end

Conclusion

Whenever you’re checking for an actual class of an object rather than examining how it behaves, take a second to think if you can duck type it instead. It will make your code more generic, and make it easier for others to use.

Again, thank you to Thoughtbot for Shoulda and I’m sorry I singled you out. Shoulda is great, and just happened to be in the wrong place at the wrong time for me to start a rant about duck typing.

comments powered by Disqus