It's Bruno

Hey, I'm Bruno 👋

I'm a software engineer with experience in design, development and testing of web-based applications.
You can find me on twitter or write an email.
September 25, 2014

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:

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

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

{% highlight ruby %} def foo_objects(objects) objects = [objects] unless objects.is_a?(Array)

objects.each {|object| object.foo! } end {% endhighlight %} Life was good, tests were passing, but wait a minute!

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

{% highlight ruby %} Array(1) #=> [1] Array([1, 2, 3]) #=> [1 ,2 ,3]

Array(“string”) #=> [“string”] Array([“string”]) #=> [“string”]

Array(object) #=> [object] Array([object]) #=> [object] {% endhighlight %}

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

{% highlight ruby %} def foo_objects(objects) Array(objects).each &:foo! end {% endhighlight %}

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

{% highlight ruby %}

WRONG: wrap something in an Array, if it isn’t an Array.

def Array(arg) arg.is_a?(Array) ? arg : [arg] end {% endhighlight %}

But I didn’t check it.

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

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

bar_objects({ shiny: “object” }) #=> Something funky happened. {% endhighlight %}

That was because: {% highlight ruby %}

Expectation (WRONG)

Array({ shiny: “object”}) #=> [{ shiny: “object” }]

Reality

Array({ shiny: “object”}) #=> [[:shiny, “object”]] # What!? Why??? {% endhighlight %}

Array(arg) was not what I had imagined. The implementation is described as follows: {% highlight ruby %} def Array(arg) arg.to_a # or whatever, it’s C code end {% endhighlight %}

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: {% highlight ruby %} 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!

{% endhighlight %}

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.

Made in São Paulo ☂️