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?
source to share
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]]}
source to share