【问题标题】:How to understand the work flow in ruby enumerator chain如何理解 ruby​​ 枚举器链中的工作流程
【发布时间】:2017-10-07 11:10:12
【问题描述】:

下面的代码产生两种不同的结果。

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]]}

我认为执行流是从右到左,数据流是从左到右。块应该作为参数从右到左传递。

使用puts,我观察到该块在内部each 中执行。

在第一条链中,group_by 应该向each 询问数据,each 将返回index%3 的结果,group_by 应该处理结果并将其交给另一个区块。但是区块是如何通过的呢?如果块在each中执行,each不会传递两个参数itemindex,而只会传递一个参数item

在第二个链中,据我了解,each_with_index 将首先接收来自each 方法的数据; each 屈服于 index%3。那样的话,each_with_index怎么处理index%3

看来我的理解有些错误。谁能详细说明这两个例子并给出这种情况下的一般工作流程?

【问题讨论】:

    标签: ruby enumerator


    【解决方案1】:

    代理对象

    执行和数据流都是从左到右的,就像 Ruby 中的任何方法调用一样。

    不过,从概念上讲,从右到左阅读 Enumerators 调用链会有所帮助,因为它们是一种 proxy object

    无块调用,它们只记得调用了哪个方法的顺序。只有在需要时才真正调用该方法,例如当 Enumerator 转换回 Array 或元素打印在屏幕上时。

    如果在链的末尾没有调用这样的方法,基本上什么都不会发生:

    [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]]
    

    这种行为使得处理非常大的对象流成为可能,而无需在方法调用之间传递巨大的数组。如果不需要输出,则不计算任何内容。如果最后需要 3 个元素,则只计算 3 个元素。

    代理模式在 Rails 中被大量使用,例如 ActiveRecord::Relation

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

    在这种情况下启动 2 个数据库查询效率会很低。不过,您只能在链式方法的末尾知道这一点。这是一个相关的答案(How does Rails ActiveRecord chain “where” clauses without multiple queries?

    我的枚举器

    这是一个快速而肮脏的MyEnumerator 课程。它可能会帮助您理解问题中方法调用的逻辑:

    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]]}
    

    【讨论】:

    • 非常感谢您的精彩解释和示例代码。运行您的示例后,我看到第一次应用后,将 group by 应用于 [e,d,c,b,a] 生成链接到方法 group_by 的枚举器。在该步骤中,不执行枚举器。在第二个应用中,将 each_index_with 应用于第一个结果(枚举器),each_index_with 产生这样的结果:'{0=>["e","b"]....}'。没有任何block经过,group_by怎么知道怎么分组呢?从枚举器到 '{0=>["e","b"]...}' 的过程很神秘!
    猜你喜欢
    • 2010-11-28
    • 1970-01-01
    • 1970-01-01
    • 2012-06-26
    • 2010-11-02
    • 2012-11-01
    • 2020-04-28
    • 1970-01-01
    • 2018-02-23
    相关资源
    最近更新 更多