Как работают счетчики в Ruby 1.9.1?
-
07-07-2019 - |
Вопрос
Этот вопрос не о том, как использовать перечислители в 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, используя fibers, а не счетчики на основе продолжения в стиле 1.8.6, чтобы все было понятно?
Решение
Вот простой перечислитель ruby, который использует волокна и должен в значительной степени вести себя как оригинал:
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
И прежде чем кто-то пожалуется на исключения как на средство управления потоком:Реальный перечислитель также вызывает 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]]
Другие советы
На самом деле в вашем e = Bunk.new.each предложение else изначально не выполняется. Вместо этого выполняется условие if! Block_given, которое возвращает объект перечислителя. Объект перечислителя действительно сохраняет объект волокна внутри. (По крайней мере, так это выглядит в enumerator.c)
Когда вы вызываете e.each, он вызывает метод для перечислителя, который использует внутреннее волокно для отслеживания его контекста выполнения. Этот метод вызывает метод Bunk.each, используя контекст выполнения волокон. Здесь вызов Bunk.each выполняет условие else и возвращает значение.
Я не знаю, как реализован yield или как волокно отслеживает контекст выполнения. Я не смотрел на этот код. Почти вся магия перечислителя и волокна реализована на C.
Вы действительно спрашиваете, как реализованы волокна и урожайность? Какой уровень детализации вы ищете?
Если я не в базе, поправьте меня.
Как отмечали другие постеры, я считаю, что это создает свое собственное "волокно" [в 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
ХТХ.
Я думаю, что это будет точнее. Вызов каждого из перечислителя должен совпадать с вызовом исходного метода итератора. Поэтому я бы немного изменил исходное решение на это:
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