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 likeeach_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
orreject
, 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).
- For methods like