brunobuccolo.com

Ruby: Nitpicking Array(arg)

This is a tale about nitpicking code…

So there was I looking at a method, that could take a single object, or an array of objects. The method just wanted to call foo! on all the objects and be done for the day:

def foo_objects(objects)
  if objects.is_a?(Array)
    objects.each {|object| object.foo! }
  else
    objects.foo!
  end
end

I did not enjoy the two branches, so I tweaked it to normalize the input:

def foo_objects(objects)
  objects = [objects] unless objects.is_a?(Array)

  objects.each {|object| object.foo! }
end

Life was good, tests were passing, but wait a minute!

Maybe there should be a native way of doing this. How about Array(arg)?

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

Array("string")   #=> ["string"]
Array(["string"]) #=> ["string"]

Array(object)   #=> [object]
Array([object]) #=> [object]

Awesome! Exactly what I wanted. I could now write a one-liner using standard Ruby foo:

def foo_objects(objects)
  Array(objects).each &:foo!
end

And this will work every time right, because this is how it must be implemented, I thought to my self:

# WRONG: wrap something in an Array, if it isn't an Array.
def Array(arg)
  arg.is_a?(Array) ? arg : [arg]
end

But I didn’t check it.

I’ve used Array(arg) in many occasions, until I was caught off-guard by this:

def bar_objects(objects)
  Array(objects).each {|object| bar(object)}
end

bar_objects({ shiny: "object" }) #=> Something funky happened.

That was because:

# Expectation (WRONG)
Array({ shiny: "object"}) #=> [{ shiny: "object" }]

# Reality
Array({ shiny: "object"}) #=> [[:shiny, "object"]] # What!? Why???

Array(arg) was not what I had imagined. The implementation is described as follows:

def Array(arg)
  arg.to_a # or whatever, it's C code
end

And Hash#to_a did that monkey business of splitting the key values pairs, instead of just wrapping itself into an array. It’s funny that I had used Hash#to_a before on other occasions, but with my mental model for Array(), such behavior just didn’t even make sense.

Today, besides Array(arg) when arg is not Hash, I also use the splat operator. But we can only rely on splat if only one of our arguments are in this object vs array of objects dual nature:

def foo_objects(*objects)
  objects.each &:foo!
end

class String
  def foo!
    print "ok!"
  end
end

argument = "a"
foo_objects(*argument) #=> ok!

argument = ["a", "b"]
foo_objects(*argument) #=> ok!ok!

If I wake up feeling pragmatic, I might just [objects].flatten and get going. But that will break if you’re supposed to receive tuples or information structured in nested arrays.

As you can see, there are many ways normalizing input. Find the QA in you, write some funky tests and keep learning the APIs!

PS: Do you have a silver bullet for this? Please share :)

UPDATE: Many of you suggested I used Array#wrap from ActiveSupport, thanks! It was also suggested that I should have clearer interfaces, when possible, to avoid dealing with this ambiguity.

Bruno Buccolo

Follow me on Twitter! @buccolo


© 2011– Bruno Buccolo | Made in São Paulo ☂