Ruby's Enumerator class is one of the most used classes of the whole Ruby ecosystem, and yet, one of the less popular. Lots of people use it without even noticing its presence.
Enumerator
First, let's prove Enumerator really exists:
p [1, 2, 3].each
# => #<Enumerator: [1, 2, 3]:each>
By printing the return value of each
without giving it a block, we can see
that each
returns an instance of Enumerator
.
And like any other instance, we can call methods on it.
Mutating an Enumerator
Enumerator
provides a few interesting methods. with_index
and with_object
let you mutate the behavior of the current Enumerator
.
[1, 2, 3].each.with_index do |item, index|
p item
p index
end
# => 1
# => 0
# => 2
# => 1
# => 3
# => 2
each_with_object_and_index
Have you ever been in the situation where you wished Ruby had a
each_with_object_and_index
method ? It happened to me a few weeks ago when I
wanted to build a hash representing Rails's params with a nested collection for
a spec purpose.
Something like that:
{
"resource" => {
"kitten_attributes" => {
"0" => {
"id" => 10,
"name" => "Ohai",
}
}
}
Given a collection of kitten
, we want to create a Hash
, with the key being
the index, and the value built from the current iteration of kitten.
# Placeholder object
Kitty = Struct.new(:id, :name)
# Our precious kitten
kitten = [Kitty.new(1, "Ohai"), Kitty.new(2, "Kitty")]
kitten_attributes = kitten.each.with_object({}).with_index do |(kitty, attributes), index|
attributes[index.to_s] = {
"id" => kitty.id,
"name" => kitty.name
}
end
attr = {
"resource" => {
"kitten_attributes" => kitten_attributes
}
}
p attr
# => {"resource"=>{"kitten_attributes"=>{"0"=>{"id"=>1, "name"=>"Ohai"},
# => "1"=>{"id"=>2, "name"=>"Kitty"}}}}
The first thing to notice is that each_with_object
could be used instead of
each.with_object
, it would give the exact same result.
Probably the most puzzling thing is the way arguments are given to the block.
|(kitty, attributes), index|
. The first argument is an Array containing the
first iteration (our precious kitty) as first element, and the accumulator
provided by with_object
as second element. So it could be |elements, index|
actually. But a small flavour of Pattern matching would make this code more
readable.
Ruby does not provide a each_with_object_and_index
method, but it provides us
the tool to easily build one by combining with_object
and with_index
.
There is much more to say about Enumerator
, but let's call it a day.