The following code, which abuses Enumerator.feed
to do something coroutine-ish, works fine:
class Foo
def initialize
@enum = Enumerator.new do |yielder|
@yielder = yielder
plus_ones = enum_for(:each_plus_one)
plus_ones.each do |n|
puts n
end
end
@enum.next
end
def <<(n)
@enum.feed(n)
@enum.next
end
def each_plus_one
loop do
n = @yielder.yield
yield n + 1
end
end
end
foo = Foo.new
foo << 1
foo << 2
foo << 3
$ ruby foo.rb
2
3
4
However, if I try to use external iteration instead, I get a confusing error:
class Foo
def initialize
@enum = Enumerator.new do |yielder|
@yielder = yielder
plus_ones = enum_for(:each_plus_one)
loop do
puts plus_ones.next
rescue StopIteration
break
end
end
@enum.next
end
...
$ ruby foo.rb
nil
foo.rb:27:in 'block in Foo#each_plus_one': undefined method '+' for nil (NoMethodError)
yield n + 1
^
from <internal:kernel>:168:in 'Kernel#loop'
from foo.rb:25:in 'Foo#each_plus_one'
from foo.rb:in 'Enumerator#each'
Why does this happen?
@yielder.yield
seems to call Fiber.yield
:
Enumerator.new do |y|
@y = y
loop { y.yield }
end.next
@y << 1
# => 'Enumerator::Yielder#<<': attempt to yield on a not resumed fiber (FiberError)
Fiber.yield
# => 'Fiber.yield': attempt to yield on a not resumed fiber (FiberError)
That means you can't call @yielder.yield
from inside another enumerator's next
call, as you won't return back to main fiber but back to that next
call.
The problem is where underlying fibers yield back to:
# when using `each`
f = Fiber.new do
loop do
p Fiber.yield # this returns back to resuming fiber (what `@yielder.yield` does)
end
end.tap(&:resume) # here in the main fiber waiting for next resume
f.resume(10) #=> 10
# when using `next`
f = Fiber.new do
enum = Fiber.new do
loop do
Fiber.yield # this returns back to resuming fiber
end
end
loop do
p enum.resume # here in Enumerator fiber (`plus_ones.next`)
# # then loop, bouncing between yield and resume, no waiting/blocking
end
end.tap(&:resume) #=> loops forever
To make it work you can transfer
from enum_for
fiber to main fiber and feed/next @enum
:
class Foo
def initialize
@main_fiber = Fiber.current
@enum = Enumerator.new do |yielder|
@enumerator_fiber = Fiber.current
loop do
p yielder.yield
end
end
@enum.next
@fiber = Fiber.new do
plus_ones = enum_for(:each_plus_one)
loop do
n = plus_ones.next
@enum.feed n
@enum.next
end
end
@fiber.transfer # lets you transfer back to main fiber
# # Or like this:
# @enum = Enumerator.new do |yielder|
# plus_ones = enum_for(:each_plus_one)
# loop do
# yielder << plus_ones.next
# end
# end
# @fiber = Fiber.new { loop { p @enum.next } }.tap(&:transfer)
end
def <<(n)
@enum_for_fiber.transfer n
end
def each_plus_one
@enum_for_fiber = Fiber.current
loop do
n = @main_fiber.transfer
yield n + 1
end
end
end
foo = Foo.new
foo << 1 #=> 2
foo << 2 #=> 3
foo << 3 #=> 4