Como Enumerators trabalho em Ruby 1.9.1?
-
07-07-2019 - |
Pergunta
Esta questão não é sobre como usar Enumerators em Ruby 1.9.1 mas estou curioso como eles funcionam. Aqui está um código:
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
No código acima eu posso usar e = Bunk.new.each
, e depois e.next
, e.next
para obter cada elemento sucessivo, mas como exatamente é isso suspensão de execução e, em seguida, retomar no ponto certo?
Estou ciente de que, se o rendimento na 0.upto
é substituído por Fiber.yield
então é fácil de entender, mas que não é o caso aqui. É um yield
velho liso, então como é que funciona?
Eu olhei para enumerator.c mas é neigh sobre incompreensível para mim. Talvez alguém poderia fornecer uma implementação em Ruby, usando fibras, não 1.8.6 estilo recenseadores baseado em continuação, que torna tudo claro?
Solução
Aqui está um recenseador rubi claro que utiliza fibras e deve praticamente se comportar como o original:
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
E antes que alguém se queixa de exceções como controle de fluxo:. O verdadeiro Enumerator levanta StopIteration no final, também, então eu apenas emulado o comportamento original
Uso:
>> 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]]
Outras dicas
Na verdade, em seu e = Bunk.new.each a cláusula else não é executado inicialmente. Em vez dos if! Block_given 'executa cláusula e retorna um objeto enumerador. O objeto enumerador faz manter um objeto de fibra internamente. (Pelo menos é o que parece no enumerator.c)
Quando você chama e.each ele está chamando um método em um recenseador que utiliza uma fibra internamente para manter o controle de seu contexto de execução. Este método chama o método Bunk.each usando o contexto de execução fibras. A chamada Bunk.each aqui faz EXECUT a cláusula else e os rendimentos até o valor.
Eu não sei como rendimento é implementado ou como uma fibra rastreia o contexto de execução. Eu não olhei para esse código. Quase toda a magia recenseador e fibra é implementado em C.
Você está realmente perguntando como fibras e rendimento são implementadas? O nível de detalhe que você está procurando?
Se estou fora da base por favor me corrigir.
Como os outros cartazes observou, eu acredito que ele cria seu próprio "fibra" privado [em 1,9]. Em 1.8.7 (ou 1.8.6 se você usar a jóia backports) de alguma forma ou de outra ele faz a mesma coisa (talvez porque os tópicos em 1.8 são o equivalente de fibras, ele só usa-los?)
Assim, em 1,9 e 1.8.x, se você encadear vários deles juntos a.each_line.map.each_with_index {}
Na verdade, flui através de toda essa cadeia com cada linha, como uma espécie de tubo na linha de comando
http://pragdave.blogs.pragprog.com /pragdave/2007/12/pipelines-using.html
HTH.
Eu acho que isso seria mais preciso. Chamando cada um na recenseador deve ser o mesmo que chamar o método iterator originais. Assim, gostaria de alterar um pouco a solução original para isso:
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