Как работают счетчики в Ruby 1.9.1? - PullRequest
8 голосов
/ 17 сентября 2009

Этот вопрос не о том, как использовать перечислители в Ruby 1.9.1, а о том, как они работают. Вот некоторый код:

class Bunk
  def initialize
    @h = [*1..100]
  end

  def each
    if !block_given?
      enum_for(:each)
    else
      0.upto(@h.length) { |i|
        yield @h[i]
      }
    end
  end
end

В приведенном выше коде я могу использовать e = Bunk.new.each, а затем e.next, e.next, чтобы получить каждый последующий элемент, но как именно он приостанавливает выполнение и затем возобновляется в нужном месте?

Мне известно, что если доходность в 0.upto заменена на Fiber.yield, то это легко понять, но здесь дело обстоит не так. Это простой старый yield, так как он работает?

Я посмотрел на enumerator.c, но мне кажется, что это непостижимо для меня. Может быть, кто-то мог бы предоставить реализацию в Ruby, используя волокна, а не перечислители на основе продолжения стиля 1.8.6, что бы все прояснило?

Ответы [ 4 ]

14 голосов
/ 17 сентября 2009

Вот простой перечислитель ruby, который использует Fibers и должен вести себя как оригинал:

class MyEnumerator
  include Enumerable

  def initialize(obj, iterator_method)
    @f = Fiber.new do
      obj.send(iterator_method) do |*args|
        Fiber.yield(*args)
      end
      raise StopIteration
    end
  end

  def next
    @f.resume
  end

  def each
    loop do
      yield self.next
    end
  rescue StopIteration
    self
  end
end

И прежде, чем кто-то будет жаловаться на исключения как управление потоком: настоящий Enumerator также вызывает StopIteration в конце, поэтому я просто эмулировал исходное поведение.

Использование:

>> enum = MyEnumerator.new([1,2,3,4], :each_with_index)
=> #<MyEnumerator:0x9d184f0 @f=#<Fiber:0x9d184dc>
>> enum.next
=> [1, 0]
>> enum.next
=> [2, 1]
>> enum.to_a
=> [[3, 2], [4, 3]]
4 голосов
/ 17 сентября 2009

На самом деле в вашем e = Bunk.new.each предложение else не выполняется изначально. Вместо этого выполняется условие if! Block_given, которое возвращает объект перечислителя. Объект перечислителя действительно сохраняет объект волокна внутри. (По крайней мере, так это выглядит в enumerator.c)

Когда вы вызываете e.each, он вызывает метод для перечислителя, который использует внутреннее волокно для отслеживания его контекста выполнения. Этот метод вызывает метод Bunk.each, используя контекст выполнения волокон. Вызов Bunk.each здесь выполняет условие else и возвращает значение.

Я не знаю, как реализован yield или как волокно отслеживает контекст выполнения. Я не смотрел на этот код. Почти вся магия перечислителя и волокна реализована на языке C.

Вы действительно спрашиваете, как реализованы волокна и урожайность? Какой уровень детализации вы ищете?

Если я не в базе, поправьте меня.

1 голос
/ 13 сентября 2011

Я думаю, это было бы точнее. Вызов каждого из перечислителя должен совпадать с вызовом исходного метода итератора. Поэтому я бы немного изменил исходное решение на это:

class MyEnumerator
  include Enumerable

   def initialize(obj, iterator_method)
    @f = Fiber.new do
      @result = obj.send(iterator_method) do |*args|
       Fiber.yield(*args)
      end
      raise StopIteration
    end
   end

   def next(result)
     @f.resume result
   end

   def each
     result = nil
     loop do
      result = yield(self.next(result))
     end
     @result
   end
end
1 голос
/ 26 сентября 2009

Как отмечали другие авторы, я считаю, что он создает свое собственное «волокно» [в 1.9]. В 1.8.7 (или 1.8.6, если вы используете гем backports) так или иначе он делает то же самое (возможно, потому что все потоки в 1.8 являются эквивалентами волокон, он просто использует их?)

Таким образом, в 1.9 и 1.8.x, если вы соедините несколько из них вместе a.each_line.map.each_with_index {}

На самом деле он проходит через всю эту цепочку с каждой строкой, как канал в командной строке

http://pragdave.blogs.pragprog.com/pragdave/2007/12/pipelines-using.html

НТН.

...