af83

Having fun with Enumerator

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.

blog comments powered by Disqus