In which a method tries to be too accepting

Posted by Ian Whitney on September 13, 2015

I frequently write methods like this:

def watch_magicians(magicians)
  magicians.collect { |magician| magician.magical_phrase }
end

a_bunch_of_magicians = [copperfield, blane, maskelyne]
watch_magicians(a_bunch_of_magicians)

Invariably I then need to watch a single magician, which I can only do like so

watch_magicians([ricky_jay])

Which seems unnecessary in a language like Ruby. If I were using type-checked language then I would write watch_magicians to require an Array. But no such requirement exists in Ruby, so why shouldn’t watch_magicians be able to work like this?

# with a collection
watch_magicians([henning, houdini])

# with just one magician
watch_magicians(jillete)

# with no magicians
watch_magicians(nil)

My first thought is that this is why the splat operator * exists, right? Let’s play with a little demonstration method.

def demo(*items)
  puts items
end

demo(1)
#=> [1]
# Yay!

demo(nil)
#=> [nil]
# Ooops

demo([1,2])
#=> [[1, 2]]
# Uh...

The second example will blow up when we try to do anything with nil, and the third example needs to be flattened before it’s useful. Using splat this way won’t help.

You could use splat outside of the method, like this:

no_items = *nil
#=> []
1_item = *1
#=> [1]
2_items = *[1,2]
#=> [1,2]

demo(no_items)
#=> []
demo(1_item)
#=> [1]
demo(2_items)
#=> [1,2]

That works, but it doesn’t really solve the problem I started with. Using splat in this way is just casting values to arrays before I call the method. I want to be able to pass non-arrays to the method and have it still work.

For our next attempt, we use to_a to convert our parameter to an array

def demo(items)
  items.to_a.each { |item| puts item }
end

But this means that everything we pass into demo now has to support to_a

demo(1)
#=> undefined method `to_a' for 1:Fixnum

So, that’s not great.

What if we create a new array?

def demo(items)
  Array.new(items).each { |item| puts item }
end

Array.new does not work that way, though:

Array.new(1)
#=> [nil]
Array.new(nil)
#=> no implicit conversion from nil to integer

So, that’s worse.

Avdi Grimm’s excellent book Confident Ruby offers a better approach, Kernel.Array:

puts Array(1)
#=> [1]

puts Array(nil)
#=> []

puts Array([1,2])
#=> [1,2]

In our demonstration method:

def demo(items)
  Array(items).each { |item| puts item }
end

demo(nil)
#=> []

demo(1)
1
=> [1]

demo([1,2])
1
2
=> [1, 2]

Great! Let’s use that in our real code:

def watch_magicians(magicians = nil)
  Array(magicians).collect { |magician| magician.magical_phrase }
end

Of course we have tests for this method. And they use a little Struct as a test double:

Magician = Struct.new(:magical_phrase)

describe "watch_magicians" do
  it "works with one magician" do
    magician = Magician.new("Alakazam!")
    expect(watch_magicians(magician)).to include("Alakazam!")
  end

  it "works with a collection of magicians" do
    magician1 = Magician.new("Alakazam!")
    magician2 = Magician.new("Shazam")
    returned = watch_magicians([magician1, magician2])
    expect(returned).to include("Alakazam!")
    expect(returned).to include("Shazam")
  end

  it "works with no magicians" do
    expect(watch_magicians).to be_empty
  end
end

Confident in our work, we run our tests. 2 of the 3 pass, but for the “one magician” test we get:

NoMethodError: undefined method `magical_phrase' for "Alakazam!":String

The hell?

Let’s look at the docs for Kernel.Array.

Returns arg as an Array. First tries to call to_ary on arg, then to_a.

In our test with a single magician we call Array on a single Magician struct. Does one of those respond to to_ary?

Magician.new("Alakazam!").respond_to?(:to_ary)
#=> false

Nope. How about to_a

Magician.new("Alakazam!").respond_to?(:to_a)
#=> true

And what does that do?

Returns the values for this struct as an Array.

Magician.new("Alakazam!").to_a
#=> ["Alakazam!"]

Well. That’s unexpected.

The quick lesson here, the one you can use to show off your sweet Ruby knowledge amongst your friends, is that Struct has a surprising implementation of to_a.

The slightly deeper lesson to learn is that Kernel.Array is great, until you pass it something that responds to to_ary or to_a in an unexpected way.

Underneath that is the design lesson: making methods that can handle any input is likely a waste of time and a source of bugs. That was certainly the case with the code I was working on this week.

The deepest lesson, the real lesson, is that flexibility like Ruby’s is a double-edged sword. As powerful, and fun and fantastic as dynamic typing is, it can cut you in goofy ways. When your programmer friends start raving about how awesome strong typing is, it’s because they never have to worry about weirdness like this.

Maybe I should re-double my efforts to learn Haskell.

postscript: the apparently undocumented behavior of Kernel.Array when an object doesn’t respond to either to_ary or to_a is to simply return that object as the only element of a new array. Such as:

1.respond_to?(:to_ary)
#=> false
1.respond_to?(:to_a)
#=> false
Array(1)
#=> [1]

Though this post is not related to my talk, I’m still plugging away on a presentation for Rocky Mountain Ruby. I’d love to see you there! Tweet or email at me if you’ll be at the conference or just want to hang out in Boulder.

Writing the presentation has curtailed my blogging and newsletter-ing a bit; sorry about that. You can read previous newsletters, or sign up for free. Comments/feedback/&c. welcome on twitter, at ian@ianwhitney.com, or leave comments on the pull request for this post.