Search code examples
rubyenumerableenumerator

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


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!


Solution

  • 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?)