Ruby enumerator chaining Ruby enumerator chaining ruby ruby

Ruby enumerator chaining


You might find it useful to break these expressions down and use IRB or PRY to see what Ruby is doing. Let's start with:

[1,2,3].each_with_index.map { |i,j| i*j }

Let

enum1 = [1,2,3].each_with_index  #=> #<Enumerator: [1, 2, 3]:each_with_index>

We can use Enumerable#to_a (or Enumerable#entries) to convert enum1 to an array to see what it will be passing to the next enumerator (or to a block if it had one):

enum1.to_a  #=> [[1, 0], [2, 1], [3, 2]]

No surprise there. But enum1 does not have a block. Instead we are sending it the method Enumerable#map:

enum2 = enum1.map  #=> #<Enumerator: #<Enumerator: [1, 2, 3]:each_with_index>:map>

You might think of this as a sort of "compound" enumerator. This enumerator does have a block, so converting it to an array will confirm that it will pass the same elements into the block as enum1 would have:

enum2.to_a  #=> [[1, 0], [2, 1], [3, 2]]

We see that the array [1,0] is the first element enum2 passes into the block. "Disambiguation" is applied to this array to assign the block variables the values:

i => 1j => 0

That is, Ruby is setting:

i,j = [1,0]

We now can invoke enum2 by sending it the method each with the block:

enum2.each { |i,j| i*j }  #=> [0, 2, 6]

Next consider:

[1,2,3].map.each_with_index { |i,j| i*j }

We have:

enum3 = [1,2,3].map  #=> #<Enumerator: [1, 2, 3]:map>enum3.to_a  #=> [1, 2, 3]enum4 = enum3.each_with_index  #=> #<Enumerator: #<Enumerator: [1, 2, 3]:map>:each_with_index>enum4.to_a  #=> [[1, 0], [2, 1], [3, 2]]enum4.each { |i,j| i*j }  #=> [0, 2, 6]

Since enum2 and enum4 pass the same elements into the block, we see this is just two ways of doing the same thing.

Here's a third equivalent chain:

[1,2,3].map.with_index { |i,j| i*j }

We have:

enum3 = [1,2,3].map  #=> #<Enumerator: [1, 2, 3]:map>enum3.to_a  #=> [1, 2, 3]enum5 = enum3.with_index  #=> #<Enumerator: #<Enumerator: [1, 2, 3]:map>:with_index>enum5.to_a  #=> [[1, 0], [2, 1], [3, 2]]enum5.each { |i,j| i*j }  #=> [0, 2, 6]

To take this one step further, suppose we had:

[1,2,3].select.with_index.with_object({}) { |(i,j),h| ... }

We have:

enum6 = [1,2,3].select  #=> #<Enumerator: [1, 2, 3]:select>enum6.to_a  #=> [1, 2, 3]enum7 = enum6.with_index  #=> #<Enumerator: #<Enumerator: [1, 2, 3]:select>:with_index>enum7.to_a  #=> [[1, 0], [2, 1], [3, 2]]enum8 = enum7.with_object({})  #=> #<Enumerator: #<Enumerator: #<Enumerator: [1, 2, 3]:  #     select>:with_index>:with_object({})>enum8.to_a  #=> [[[1, 0], {}], [[2, 1], {}], [[3, 2], {}]]

The first element enum8 passes into the block is the array:

(i,j),h = [[1, 0], {}]

Disambiguation is then applied to assign values to the block variables:

i => 1j => 0h => {}

Note that enum8 shows an empty hash being passed in each of the three elements of enum8.to_a, but of course that's only because Ruby doesn't know what the hash will look like after the first element is passed in.


Methods you are mentioning are defined on Enumerable objects. These methods behave differently depending on whether you pass a block or not.

  • When you do not pass a block, they typically return an Enumerator object, to which you can chain further methods like each_with_index, with_index, map, etc.
  • When you pass a block to these methods, they return different kinds of object depending on what will make sense for that particular method.
    • For methods like find, its purpose is to find the first object that satisfies a condition, and it does not make particular sense to wrap that in an array, so it returns that object bare.
    • For methods like select or reject, their purpose is to return all relevant objects, so they cannot return a single object, and they have to be wrapped in an array (even when the relevant object happens to be a single object).