Today I learned a new trick in Ruby that is useful for minimizing nested blocks.
Let’s say you are trying to create a list of mutant animals from two arrays. One
array contains animals. The other array contains modifiers on those animals.
Here’s how you might generate them using two Enumberable#collect
calls:
animals = ["Platypus", "Tiger", "Squirrel"]
modifiers = ["Duck-billed", "Bengal", "Flying"]
animals.collect do |animal|
modifiers.collect do |modifier|
"#{modifier} #{animal}"
end
end.flatten
# => ["Duck-billed Platypus",
# "Bengal Platypus",
# "Flying Platypus",
# "Duck-billed Tiger",
# "Bengal Tiger",
# "Flying Tiger",
# "Duck-billed Squirrel",
# "Bengal Squirrel",
# "Flying Squirrel"]
That works just fine, but nested blocks can quickly become hard to follow. Also, that trailing .flatten
can be easily missed when scanning the code.
This is where Array#product
comes into play. Array#product
gives every possible
combination of elements from all arrays. With this, your mutant-animal generator
can be re-written:
animals = ["Platypus", "Tiger", "Squirrel"]
modifiers = ["Duck-billed", "Bengal", "Flying"]
animals.product(modifiers).collect do |animal, modifier|
"#{modifier} #{animal}"
end
# => ["Duck-billed Platypus",
# "Bengal Platypus",
# "Flying Platypus",
# "Duck-billed Tiger",
# "Bengal Tiger",
# "Flying Tiger",
# "Duck-billed Squirrel",
# "Bengal Squirrel",
# "Flying Squirrel"]
The nested block is eliminated, and that easy-to-miss trailing .flatten
is gone. Hooray.
I still don’t quite understand how collect
knows to look at array elements when the passed block takes two arguments instead of one. Can’t find anything that alludes to this in the docs.
It’s some kind of magic.
Published: 2013-09-13