Reputation: 1492
In this example,
[1, 2, 3].each_with_index.map{|i, j| i * j}
# => [0, 2, 6]
my understanding is that, since each_with_index
enumerator is chained to map
, map
behaves like each_with_index
by passing an index inside the block, and returns a new array.
For this,
[1, 2, 3].map.each_with_index{|i, j| i * j}
# => [0, 2, 6]
I'm not sure how to I interpret it.
In this example,
[1, 2, 3, 4].map.find {|i| i == 2}
# => 2
I was expecting the the output to be [2]
, assuming that map
is chained to find
, and map
would return a new array.
Also, I see this:
[1, 2, 3, 4].find.each_with_object([]){|i, j| j.push(i)}
# => [1]
[1, 2, 3, 4].each_with_object([]).find{|i, j| i == 3}
# => [3, []]
Can you let me know how to interpret and understand enumerator chains in Ruby?
Upvotes: 10
Views: 2658
Reputation: 110755
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 => 1
j => 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 => 1
j => 0
h => {}
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.
Upvotes: 17
Reputation: 168249
Methods you are mentioning are defined on Enumerable
objects. These methods behave differently depending on whether you pass a block or not.
Enumerator
object, to which you can chain further methods like each_with_index
, with_index
, map
, etc.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.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).Upvotes: 1