Quack, quack! (Duck Typing)

I'm pondering Duck Typing tonight as I write event classes and their associated specs.

Here's an abbreviated spec for the WindowResized event, which holds an [x, y] array with the new size of the window:

it "should reject non-array objects as size" do
    lambda { WindowResized.new( 20 ) }.should raise_error(ArgumentError)
end

Giving the number 20 as the size doesn't make sense, and you'd run into an error sooner or later. In this case, without any checks, the error would probably show up when you tried to set the Screen mode again to the new size. Because of the delayed effect of the event queue, the backtrace wouldn't have anything to do with the real problem — that you gave the event an invalid argument.

So, the rationale behind this spec is that the class should complain loudly at the soonest opportunity, so that the location of the error is easy to find.

To make this spec pass, I might write the following:

# this is not duck typing
unless size.kind_of? Array
    raise ArgumentError, "size has to be an Array"
end
@size = size

The spec would pass, but the code would be pretty inflexible. Suppose someone wrote a Size class which had 2 elements, but wasn't actually an Array. This check would reject it, even though it might have been usable. A conundrum!

Duck typing to the rescue, right?

After thinking about it for a while, I realized that the size parameter doesn't have to be an Array per se, it just has to be something that can be converted into an Array before I store it. Since anything that can be converted to an array should have a #to_ary method, I could check that it has that:

# getting warmer...
unless size.respond_to?( :to_ary )
    raise ArgumentError, "size has to be convertable to an Array"
end
@size = size.to_ary

The spec still passes, and I'm getting more duck-typey. Or at least chicken-typey. We could go a step further:

# quack, quack!
@size = size.to_ary

Since the goal was just to complain loudly if the argument is invalid, and we've decided that an argument is invalid if it can't be converted to an Array, then we don't really need to check that it responds to anything. Just try calling the method! If it doesn't have that method, you'll get an error, just like you're supposed to (although we'd have to tweak the spec to expect NoMethodError instead of ArgumentError). If it does have the method, you have a (presumably) valid parameter. So, it's all good.

It would be even more duck-typey to have no checks at all (and no specs for the checks), and instead just assume that everybody who uses the method has read the docs and knows to pass the right sort of thing. But as I explained earlier, not checking is not a good idea here. (Well, maybe in this specific case it would work out fine, since users wouldn't usually be creating their own instances of this event. But in general, no.)

So, what's my point? I don't have one. I just wrote this as a thought experiment to help me decide what to do. Thanks for listening!


Comments

Have something interesting to say about this post? Email your thoughtful comments to comments@rubygame.org.