Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Enumerator.new work with block passed?

I struggle a little bit with understanding how the Enumerator.new method works. Assuming example from documentation:

fib = Enumerator.new do |y|
  a = b = 1
  loop do
    y << a
    a, b = b, a + b
  end
end

p fib.take(10) # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Where's the loop break condition, how does it know how many times loop should to iterate (as it doesn't have any explicit break condition and looks like infinite loop) ?

like image 737
Leszek Andrukanis Avatar asked May 21 '14 12:05

Leszek Andrukanis


2 Answers

Enumerator uses Fibers internally. Your example is equivalent to:

require 'fiber'

fiber = Fiber.new do
  a = b = 1
  loop do
    Fiber.yield a
    a, b = b, a + b
  end
end

10.times.map { fiber.resume }
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
like image 147
Stefan Avatar answered Oct 04 '22 15:10

Stefan


Edit I think I understand your question now, I will still keep my original answer below.

y << a is an alias for y.yield(a), which is basically a sleep with return value. Each time a value is requested from the enumerator with next, the execution is continued until another value is yielded.


Enumerators do not need to enumerate a finite number of elements, so they are infinite. For example, fib.to_a will never terminate, because it tries to build an array with an infinite number of elements.

As such, enumerators are great as a representation of infinite series such as the natural numbers, or in your case, the fibonacci numbers. The user of the enumerator can decide how many values it needs, so in your example take(10) determines the break condition if you will.

The break condition itself is in the implementation of Enumerator#take. For demonstration purposes, we can make our own implementation called my_take:

class Enumerator
  def my_take(n)
    result = []
    n.times do
      result << self.next
    end
    result
  end
end

Where you could of course "mentally substitute" your n.times loop with your classical C style for (i=0; i<n; i++). There's your break condition. self.next is the method to get the next value of the enumerator, which you can also use outside of the class:

fib.next
#=> 1
fib.next
#=> 1
fib.next
#=> 2
fib.next
#=> 3

That said, you can of course build an enumerator that enumerates a finite number of values, such as the natural numbers in a given range, but that's not the case here. Then, the Enumerator will raise a StopIteration error when you try to call next, but all values already have been enumerated. In that case, you have two break conditions, so to speak; the one that breaks earlier will then win. take actually handles that by rescuing from the error, so the following code is a bit closer to the real implementation (however, take is in fact implemented in C).

class Enumerator
  def my_take(n)
    result = []
    n.times do
      result << self.next
    end
    result
  rescue StopIteration
    # enumerator stopped early
    result
  end
end
like image 37
Patrick Oscity Avatar answered Oct 04 '22 15:10

Patrick Oscity