Ruby enumerator chaining
Asked Answered
F

2

10

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?

Fleshy answered 13/6, 2014 at 7:30 Comment(0)
H
17

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.

Hautbois answered 13/6, 2014 at 9:8 Comment(1)
very well put forwardFleshy
L
1

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).
Libove answered 13/6, 2014 at 7:42 Comment(1)
I got that point but how do you interpret the chain like i said above [1,2,3].each_with_index.map { |i,j| i*j } and [1,2,3].map.each_with_index { |i,j| i*j } return exactly same result what am I suppose to believe thenFleshy

© 2022 - 2024 — McMap. All rights reserved.