Как понять рабочий процесс в цепочке перечислителей ruby

Приведенный ниже код дает два разных результата.

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 не будет передавать два параметра item и index, а только один параметр item.

Во второй цепочке, в моем понимании, each_with_index сначала получит данные от each метода; each уступает место index%3. В таком случае, как each_with_index может обработать index%3?

Кажется, мое понимание как-то неправильно. Может ли кто-нибудь подробно проиллюстрировать эти два примера и дать общий рабочий процесс в таких случаях?


person pyb1993    schedule 09.05.2017    source источник


Ответы (1)


Прокси-объекты

И выполнение, и потоки данных идут слева направо, как и при вызове любого метода в Ruby.

Концептуально это может помочь читать цепочки вызовов Enumerator справа налево, поскольку они представляют собой своего рода прокси-объект.

Вызванные без блока, они просто запоминают, в каком порядке был вызван какой метод. Затем метод действительно вызывается только тогда, когда это необходимо, например, когда 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 запроса к БД. Однако вы можете узнать это только в конце цепных методов. Вот соответствующий ответ (Как Rails ActiveRecord связывает предложения «где» без нескольких запросов?)

Мой Перечислитель

Вот быстрый и грязный класс 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]]}
person Eric Duminil    schedule 09.05.2017
comment
Я очень благодарен за ваше прекрасное объяснение и пример кода. После запуска вашего примера я вижу, что после первого применения применение группы к [e, d, c, b, a] создает перечислитель, который связан с методом группа по. На этом шаге перечислитель не выполняется. Во втором приложении, применяя each_index_with к первому результату (перечислителю), each_index_with дает такой результат: '{0=>[e,b]....}'. Без какого-либо пропущенного блока, как group_by знает, как группировать? Процесс перехода от перечислителя к '{0=›[e,b]...}' загадочен! - person pyb1993; 09.05.2017