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:
<span class="nb">p</span> <span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="o">].</span><span class="n">each</span><span class="c1"># => #<Enumerator: [1, 2, 3]:each></span>
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
.
<span class="o">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="o">].</span><span class="n">each</span><span class="o">.</span><span class="n">with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="p">,</span> <span class="n">index</span><span class="o">|</span> <span class="nb">p</span> <span class="n">item</span> <span class="nb">p</span> <span class="n">index</span><span class="k">end</span><span class="c1"># => 1</span><span class="c1"># => 0</span><span class="c1"># => 2</span><span class="c1"># => 1</span><span class="c1"># => 3</span><span class="c1"># => 2</span>
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:
<span class="p">{</span> <span class="s2">"resource"</span> <span class="o">=></span> <span class="p">{</span> <span class="s2">"kitten_attributes"</span> <span class="o">=></span> <span class="p">{</span> <span class="s2">"0"</span> <span class="o">=></span> <span class="p">{</span> <span class="s2">"id"</span> <span class="o">=></span> <span class="mi">10</span><span class="p">,</span> <span class="s2">"name"</span> <span class="o">=></span> <span class="s2">"Ohai"</span><span class="p">,</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span>
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.
<span class="c1"># Placeholder object</span><span class="no">Kitty</span> <span class="o">=</span> <span class="no">Struct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:id</span><span class="p">,</span> <span class="ss">:name</span><span class="p">)</span><span class="c1"># Our precious kitten</span><span class="n">kitten</span> <span class="o">=</span> <span class="o">[</span><span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s2">"Ohai"</span><span class="p">),</span> <span class="no">Kitty</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="s2">"Kitty"</span><span class="p">)</span><span class="o">]</span><span class="n">kitten_attributes</span> <span class="o">=</span> <span class="n">kitten</span><span class="o">.</span><span class="n">each</span><span class="o">.</span><span class="n">with_object</span><span class="p">({})</span><span class="o">.</span><span class="n">with_index</span> <span class="k">do</span> <span class="o">|</span><span class="p">(</span><span class="n">kitty</span><span class="p">,</span> <span class="n">attributes</span><span class="p">),</span> <span class="n">index</span><span class="o">|</span> <span class="n">attributes</span><span class="o">[</span><span class="n">index</span><span class="o">.</span><span class="n">to_s</span><span class="o">]</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"id"</span> <span class="o">=></span> <span class="n">kitty</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="s2">"name"</span> <span class="o">=></span> <span class="n">kitty</span><span class="o">.</span><span class="n">name</span> <span class="p">}</span><span class="k">end</span><span class="kp">attr</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"resource"</span> <span class="o">=></span> <span class="p">{</span> <span class="s2">"kitten_attributes"</span> <span class="o">=></span> <span class="n">kitten_attributes</span> <span class="p">}</span><span class="p">}</span><span class="nb">p</span> <span class="kp">attr</span><span class="c1"># => {"resource"=>{"kitten_attributes"=>{"0"=>{"id"=>1, "name"=>"Ohai"},</span><span class="c1"># => "1"=>{"id"=>2, "name"=>"Kitty"}}}}</span>
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.