Enumerator#each перезапускает последовательность

Я удивлен, что Enumerator#each не начинается с текущей позиции в последовательности.

o = Object.new

def o.each
  yield 1
  yield 2
  yield 3
end

e = o.to_enum
puts e.next
puts e.next
e.each{|x| puts x}
# I expect to see 1,2,3 but I see 1,2,1,2,3
# apparently Enumerator's each (inherited from Enumerable) restarts the sequence!

Я делаю это неправильно? Есть ли способ построить еще один Enumerator (из e), который будет иметь ожидаемое каждое поведение?


person Bill Burcham    schedule 05.05.2013    source источник
comment
Я не совсем понимаю, почему вы ожидаете увидеть 1,2,3 - вы увидите 1,2,1,2,3, если начнете, например, с массив [1,2,3] вместо вашего примера, поэтому конструкция вашего объекта в порядке, но если вы сделаете e.each{|x| next if x < 3; puts x; }, в цикле будет выведено только 3. next и each не взаимодействуют так, как вы ожидаете.   -  person Neil Slater    schedule 05.05.2013
comment
@NeilSlater Я ожидаю, что #each будет учитывать состояние перечислителя, потому что это метод перечислителя. Я удивлен, что Enumerator#each, по сути, дает мне то же поведение, что и исходный объект #each.   -  person Bill Burcham    schedule 06.05.2013


Ответы (2)


Вы не делаете это неправильно, просто это не та семантика, которая определена для Enumerator#each. Вы можете создать производный перечислитель, который выполняет итерацию только от текущей позиции до конца:

class Enumerator
  def enum_the_rest
    Enumerator.new { |y| loop { y << self.next } }
  end
end

o = Object.new
def o.each
  yield 1
  yield 2
  yield 3
end

e = o.to_enum
=> #<Enumerator: ...>
e.next
=> 1
e2 = e.enum_the_rest
=> #<Enumerator: ...>
e2.each { |x| puts x }
=> 2
=> 3

И, кстати, each не перезапускает последовательность, она просто всегда выполняется по всему промежутку. Ваш счетчик все еще знает, где он находится по отношению к следующему вызову next.

e3 = o.to_enum
e3.next
=> 1
e3.next
=> 2
e3.map(&:to_s)
=> ["1", "2", "3"]
e3.next
=> 3
person dbenhur    schedule 05.05.2013
comment
Было бы еще немного лучше: class Enumerator; def each_remaining; return enum_for(__method__) unless block_given?; loop{ yield self.next};end;end - person Bill Burcham; 06.05.2013

Enumerator#next и Enumerator#each работают с объектом по-разному. Согласно документации для #each (выделено мной):

Перебирает блок в соответствии с тем, как этот Enumerable был сконструирован. Если блок не задан, возвращает self.

Таким образом, #each всегда ведет себя на основе исходной настройки, а не текущего внутреннего состояния. Если вы быстро взглянете на источник, вы увидите, что rb_obj_dup вызывается для настройки нового счетчика.

person Aaron K    schedule 05.05.2013
comment
Интересно, что перечислители, кажется, не поддерживают dup после того, как они продвинулись с самого начала: e=o.to_enum; e.dup работает без жалоб, e=o.to_enum; e.next; e.dup получает TypeError: не может скопировать контекст выполнения - person dbenhur; 06.05.2013
comment
@dbenhur Да, я плохо понимаю, почему это может быть. Я предполагаю, что это, вероятно, проблема с волокном, созданным под капотом. После вызова next поведение dup может быть неоднозначным. Вы создаете новое волокно в той же точке, вам просто нужен дубликат исходного состояния и т. д.? - person Aaron K; 06.05.2013
comment
Спасибо за указание на эту документацию @AaronK. Я читал это, но вы заставили меня прочитать это снова, и теперь я понимаю это. Я все еще думаю, что для Enumerator#each неправильно не учитывать состояние Enumerator. Я был бы счастливее, если бы поведение и документ изменились :) - person Bill Burcham; 06.05.2013