How does to_enum(:method) receive its block here?
Asked Answered
W

3

6

This code, from an example I found, counts the number of elements in the array which are equal to their index. But how ?

[4, 1, 2, 0].to_enum(:count).each_with_index{|elem, index| elem == index}

I could not have done it only with chaining, and the order of evaluation within the chain is confusing.

What I understand is we're using the overload of Enumerable#count which, if a block is given, counts the number of elements yielding a true value. I see that each_with_index has the logic for whether the item is equal to it's index.

What I don't understand is how each_with_index becomes the block argument of count, or why the each_with_index works as though it was called directly on [4,1,2,0]. If map_with_index existed, I could have done:

[4,1,2,0].map_with_index{ |e,i| e==i ? e : nil}.compact

but help me understand this enumerable-based style please - it's elegant!

Wits answered 21/10, 2013 at 15:47 Comment(3)
[4,1,2,0].map.with_index{ |e,i| e==i ? e : nil}.compactFranz
Or even: [4,1,2,0].select.with_index{ |e,i| e==i}Franz
@SergioTulentsev, nice alternatives.. Are the return values from the farthest right block passed directly into 'select' iterator returned from select ? Does it matter how many are chained between ?Wits
G
2

The answer is but a click away: the documentation for Enumerator:

Most [Enumerator] methods [but presumably also Kernel#to_enum and Kernel#enum_for] have two forms: a block form where the contents are evaluated for each item in the enumeration, and a non-block form which returns a new Enumerator wrapping the iteration.

It is the second that applies here:

enum = [4, 1, 2, 0].to_enum(:count) # => #<Enumerator: [4, 1, 2, 0]:count> 
enum.class # => Enumerator
enum_ewi = enum.each_with_index
  # => #<Enumerator: #<Enumerator: [4, 1, 2, 0]:count>:each_with_index> 
enum_ewi.class #  => Enumerator
enum_ewi.each {|elem, index| elem == index} # => 2

Note in particular irb's return from the third line. It goes on say, "This allows you to chain Enumerators together." and gives map.with_index as an example.

Why stop here?

    enum_ewi == enum_ewi.each.each.each # => true
    yet_another = enum_ewi.each_with_index
       # => #<Enumerator: #<Enumerator: #<Enumerator: [4, 1, 2, 0]:count>:each_with_index>:each_with_index>    
    yet_another.each_with_index {|e,i| puts "e = #{e}, i = #{i}"}

    e = [4, 0], i = 0
    e = [1, 1], i = 1
    e = [2, 2], i = 2
    e = [0, 3], i = 3

    yet_another.each_with_index {|e,i| e.first.first == i} # => 2

(Edit 1: replaced example from docs with one pertinent to the question. Edit 2: added "Why stop here?)

Goulette answered 21/10, 2013 at 19:5 Comment(1)
Accepted, to save you some time Cary :) Nice examples, and thanks for originally finding the part of the docs that were most helpful.Wits
B
4

Let's start with a simpler example:

[4, 1, 2, 0].count{|elem| elem == 4}
=> 1

So here the count method returns 1 since the block returns true for one element of the array (the first one).

Now let's look at your code. First, Ruby creates an enumerator object when we call to_enum:

[4, 1, 2, 0].to_enum(:count)
=> #<Enumerator: [4, 1, 2, 0]:count>

Here the enumerator is waiting to execute the iteration, using the [4, 1, 2, 0] array and the count method. Enumerators are like a pending iteration, waiting to happen later.

Next, you call the each_with_index method on the enumerator, and provide a block:

...each_with_index{|elem, index| elem == index}

This calls the Enumerator#each_with_index method on the enumerator object you created above. What Enumerator#each_with_index does is start the pending iteration, using the given block. But it also passes an index value to the block, along with the values from the iteration. Since the pending iteration was setup to use the count method, the enumerator will call Array#count. This passes each element from the array back to the enumerator, which passes them into the block along with the index. Finally, Array#count counts up the true values, just like with the simpler example above.

For me the key to understanding this is that you're using the Enumerator#each_with_index method.

Barnacle answered 21/10, 2013 at 22:25 Comment(3)
This was the kind of explanation I was looking for - the distinction between setting up the pending iterator, and the passing of the block to that iterator which each_with_index does made it clear. Definite upvote @Sergio !Wits
Yes, Dean, it's a great edit, but you can't award @Sergio points for it.Goulette
@CarySwoveland: right, I love getting credit, but I also love to earn it :)Franz
G
2

The answer is but a click away: the documentation for Enumerator:

Most [Enumerator] methods [but presumably also Kernel#to_enum and Kernel#enum_for] have two forms: a block form where the contents are evaluated for each item in the enumeration, and a non-block form which returns a new Enumerator wrapping the iteration.

It is the second that applies here:

enum = [4, 1, 2, 0].to_enum(:count) # => #<Enumerator: [4, 1, 2, 0]:count> 
enum.class # => Enumerator
enum_ewi = enum.each_with_index
  # => #<Enumerator: #<Enumerator: [4, 1, 2, 0]:count>:each_with_index> 
enum_ewi.class #  => Enumerator
enum_ewi.each {|elem, index| elem == index} # => 2

Note in particular irb's return from the third line. It goes on say, "This allows you to chain Enumerators together." and gives map.with_index as an example.

Why stop here?

    enum_ewi == enum_ewi.each.each.each # => true
    yet_another = enum_ewi.each_with_index
       # => #<Enumerator: #<Enumerator: #<Enumerator: [4, 1, 2, 0]:count>:each_with_index>:each_with_index>    
    yet_another.each_with_index {|e,i| puts "e = #{e}, i = #{i}"}

    e = [4, 0], i = 0
    e = [1, 1], i = 1
    e = [2, 2], i = 2
    e = [0, 3], i = 3

    yet_another.each_with_index {|e,i| e.first.first == i} # => 2

(Edit 1: replaced example from docs with one pertinent to the question. Edit 2: added "Why stop here?)

Goulette answered 21/10, 2013 at 19:5 Comment(1)
Accepted, to save you some time Cary :) Nice examples, and thanks for originally finding the part of the docs that were most helpful.Wits
W
0

Nice answer @Cary.. I'm not exactly sure how the block makes its way through the chain of objects, but despite appearances, the block is being executed by the count method, as in this stack trace, even though its variables are bound to those yielded by each_with_index

enum = [4, 1, 2, 0].to_enum(:count)
enum.each_with_index{|e,i| raise "--" if i==3; puts e; e == i}
4
1
2
RuntimeError: --
    from (irb):243:in `block in irb_binding'
    from (irb):243:in `count'
    from (irb):243:in `each_with_index'
    from (irb):243
Wits answered 21/10, 2013 at 22:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.