RolandXu
RolandXu

Reputation: 3678

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.

Upvotes: 1

Views: 144

Answers (1)

Amadan
Amadan

Reputation: 198314

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

Upvotes: 4

Related Questions