RolandXu RolandXu - 3 months ago 21
Ruby Question

How Ruby implements Enumerator#next method?

class MyString
include Enumerable
def initialize(n)
@num = n
end
def each
i = 0
while i < @num
yield "#{i} within while"
puts "After yield #{i}"
i += 1
end
end
end

s = MyString.new(10)
a = s.to_enum
puts "first"
puts a.next
puts "second"
puts a.next


My ruby version is 2.2.5, and outputs of codes are


first

0 within while

second

After yield 0

1 within while


I think the execution flow is
first a.next->s.each->while->yield->second a.next->jump into while loop

My question is how Enumerator#next method is implemented?

I probably know there are break in block yield invoked, which cause
yield->second a.next
; however, I don't understand how second a.next can jump back into a while loop.

Answer

I don't understand how second a.next can jump back into a while loop.

Magic. Enumerator's (and Fiber's) superpowers.

These two classes were introduced in Ruby 1.9, and share many similarities; in particular, they allow you to do manual co-operative green-threading.

Let's look at fibers first, as they are more basic:

f = Fiber.new do
  puts "A"
  Fiber.yield 1
  puts "B"
  Fiber.yield 2
  puts "C"
end

puts "First"    # First
puts f.resume   # A
                # 1
puts "Second"   # Second
puts f.resume   # B
                # 2
puts "End"      # End
f.resume        # C
f.resume        # FiberError: dead fiber called

Basically, a fiber is like a thread, but it will pause whenever it yields by Fiber.yield, and resume whenever it is resumed by Fiber#resume. It is implemented in C as basic capability of Ruby, so as a student of Ruby (as opposed to student of Ruby interpreter) you don't need to know how it works, just that it does (just like you need to know IO#read will read a file, but not necessarily how it is implemented in C).

Enumerator is almost the same concept, but adapted for iteration (whereas Fiber is more multi-purpose). In fact, we can write the above almost exactly word-for-word the same with an Enumerator:

e = Enumerator.new do |yielder|
  puts "A"
  yielder.yield 1
  puts "B"
  yielder.yield 2
  puts "C"
end

puts "First"    # First
puts e.next     # A
                # 1
puts "Second"   # Second
puts e.next     # B
                # 2
puts "End"      # End
e.next          # C
                # StopIteration: iteration reached an end