Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby enumerator chaining

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?

like image 519
Ratatouille Avatar asked Jun 13 '14 07:06

Ratatouille


1 Answers

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.

like image 182
Cary Swoveland Avatar answered Sep 28 '22 07:09

Cary Swoveland