How to understand the workflow in a chain of ruby ​​enumerators

The code below gives two different results.

letters = %w(e d c b a)

letters.group_by.each_with_index { |item, index| index % 3 }
#=> {0=>["e", "b"], 1=>["d", "a"], 2=>["c"]}

letters.each_with_index.group_by { |item, index| index % 3 }
#=> {0=>[["e", 0], ["b", 3]], 1=>[["d", 1], ["a", 4]], 2=>[["c", 2]]}

      

I think the flow of execution is right to left and the flow of data is left to right. The block must be passed as a parameter from right to left.

Using puts

, I noticed that the block is being executed internally each

.

In the first chain, you group_by

should query each

for data, each

return the result index%3

, and group_by

process the result and pass it on to another block. But how is the block going? If the block is executed in each

, each

it will not pass two parameters item

and index

, but only one parameter item

.

In the second chain, in my opinion, each_with_index

first get the data from the method each

; each

gives index%3

. In this case, how to each_with_index

handle index%3

?

It seems my understanding is somehow wrong. Can someone illustrate the thesis with two examples with details and give a general workflow in such cases?

+3


source to share


1 answer


Proxy objects

Both threads of execution and data flow from left to right, as with any method call in Ruby.

Conceptually, this can help you read call chains Enumerator

from right to left, even though they are a kind of proxy object .

Called without a block, they just remember the order in which this method was called. This method is then only really called when needed, such as when Enumerator

converted back to Array

or items are printed to the screen.

If such a method is not called at the end of the chain, basically nothing happens:

[1,2,3].each_with_index.each_with_index.each_with_index.each_with_index
# #<Enumerator: ...>

[1,2,3].each_with_index.each_with_index.each_with_index.each_with_index.to_a
# [[[[[1, 0], 0], 0], 0], [[[[2, 1], 1], 1], 1], [[[[3, 2], 2], 2], 2]]

      

This behavior allows you to work with very large streams of objects, without having to pass huge arrays between method calls. If no output is needed, nothing is calculated. If 3 items are required at the end, only 3 items are evaluated.



The proxy pattern is heavily used in Rails, for example with ActiveRecord::Relation

:

@person = Person.where(name: "Jason").where(age: 26)

      

In this case, it would be inefficient to run 2 DB queries. You can only know what's the end of the chained methods. Here is a related answer ( How does the Rails ActiveRecord "where" chaining work without multiple requests?

MyEnumerator

Here's a quick and dirty class MyEnumerator

. This might help you understand the logic behind method calls in your question:

class MyEnumerator < Array
  def initialize(*p)
    @methods = []
    @blocks = []
    super
  end

  def group_by(&b)
    save_method_and_block(__method__, &b)
    self
  end

  def each_with_index(&b)
    save_method_and_block(__method__, &b)
    self
  end

  def to_s
    "MyEnumerable object #{inspect} with methods : #{@methods} and #{@blocks}"
  end

  def apply
    result = to_a
    puts "Starting with #{result}"
    @methods.zip(@blocks).each do |m, b|
      if b
        puts "Apply method #{m} with block #{b} to #{result}"
      else
        puts "Apply method #{m} without block to #{result}"
      end
      result = result.send(m, &b)
    end
    result
  end

  private

  def save_method_and_block(method, &b)
    @methods << method
    @blocks << b
  end
end

letters = %w[e d c b a]

puts MyEnumerator.new(letters).group_by.each_with_index { |_, i| i % 3 }.to_s
# MyEnumerable object ["e", "d", "c", "b", "a"] with methods : [:group_by, :each_with_index] and [nil, #<Proc:0x00000001da2518@my_enumerator.rb:35>]
puts MyEnumerator.new(letters).group_by.each_with_index { |_, i| i % 3 }.apply
# Starting with ["e", "d", "c", "b", "a"]
# Apply method group_by without block to ["e", "d", "c", "b", "a"]
# Apply method each_with_index with block #<Proc:0x00000000e2cb38@my_enumerator.rb:42> to #<Enumerator:0x00000000e2c610>
# {0=>["e", "b"], 1=>["d", "a"], 2=>["c"]}

puts MyEnumerator.new(letters).each_with_index.group_by { |_item, index| index % 3 }.to_s
# MyEnumerable object ["e", "d", "c", "b", "a"] with methods : [:each_with_index, :group_by] and [nil, #<Proc:0x0000000266c220@my_enumerator.rb:48>]
puts MyEnumerator.new(letters).each_with_index.group_by { |_item, index| index % 3 }.apply
# Apply method each_with_index without block to ["e", "d", "c", "b", "a"]
# Apply method group_by with block #<Proc:0x0000000266bd70@my_enumerator.rb:50> to #<Enumerator:0x0000000266b938>
# {0=>[["e", 0], ["b", 3]], 1=>[["d", 1], ["a", 4]], 2=>[["c", 2]]}

      

0


source







All Articles