Fibers, Procs and Enumerables
Last day I spend some hours investigating a bit deeply about Enumerable
. It is that kind of stuff that shows the magic of the Ruby language. Once you have the whole control of enumerables, you feel the power of being a Ruby developer.
Fibers, Procs and Enumerables are different things in Ruby. But I will resolve the very traditional example of showing prime numbers with each one of them.
We will define a module with the common code to find the prime numbers:
module PrimeFinder
private
def find_next_prime(x)
x += 1 while !prime?(x)
x
end
def prime?(x)
i = 2
while i <= Math.sqrt(x)
return false if x % i == 0
i += 1
end
true
end
end
Proc
Proc
saves the last state of the x
variable, so in every call
it will return the value of the next prime number:
class Prime
include PrimeFinder
def proc
x = 1
Proc.new do
x = find_next_prime(x+1)
end
end
end
irb> p = Prime.new.proc
=> #<Proc:test/fiber.rb>
irb> p.call
=> 2
irb> p.call
=> 3
irb> p.call
=> 5
irb> p.call
=> 7
irb> 5.times { puts p.call }
11
13
17
19
23
=> 5
Proc
objects picks up the surrounding environment. Any variables that are visible when a Proc
is created remain visible inside the Proc
when it is run. That’s why x
inside the Proc
object is the same as the x
outside.
Also, a Proc
always returns the last value computed in the code block. This is because Proc
objects have a lot in common with methods, so the last expression in the block will be the returned value of call
.
In summary, this solution does the trick, but it doesn’t seem the best solution for this issue.
Fiber
Fibers are light weight primitives in the Ruby standard library which can be paused, resumed and scheduled manually. Fibers are commonly used in combination of concurrence processes, because they simplify asynchronous code.
In this case, it will just return next prime number and pause the loop.
class Prime
include PrimeFinder
def fiber
Fiber.new do
x = 1
loop do
Fiber.yield x = find_next_prime(x+1)
end
end
end
end
irb> f = Prime.new.fiber
=> #<Fiber:(irb):3 (created)>
irb> f2 = Prime.new.fiber
=> #<Fiber:(irb):3 (created)>
irb> f.resume
=> 2
irb> f.resume
=> 3
irb> 3.times { puts f.resume }
5
7
11
=> 3
irb> 3.times { puts f2.resume }
2
3
5
=> 3
Basically, Fiber#yield
returns control back to the context that resumed the Fiber and returns the value which was passed to Fiber#resume
. Each defined fiber, will control its own independent state. That’s why it is useful in concurrence executions.
Enumerable
Option A
The traditional way with the include Enumerable
and defining an each
method. This way, we can use all the methods for enumerables like take
, first
, etc.
class Prime
include PrimeFinder
include Enumerable
def each(&block)
x = 1
loop do
yield x = find_next_prime(x+1)
end
end
end
Note that using take
and first
of Enumerable always start by the first prime number 2
. But if we use to_enum
, the result saves the last state of the loop.
irb> p = Prime.new
=> #<Prime:>
irb> p.take(3)
=> [2, 3, 5]
irb> p.first(3)
=> [2, 3, 5]
irb> e = p.to_enum
=> #<Enumerator: #<Prime:>:each>
irb> e.next
=> 2
irb> e.next
=> 3
irb> e.next
=> 5
irb> e.next
=> 7
irb> e.next
=> 11
Option B
Instead of using the include Enumerable
, we define an enumerable for an specific method. Is the same as using to_enum
in the last example.
class Prime
include PrimeFinder
def enum
x = 1
return enum_for(:enum) if !block_given?
loop do
yield x = find_next_prime(x+1)
end
end
end
irb> p = Prime.new.enum
=> #<Enumerator: #<Prime:>:enum>
irb> p.next
=> 2
irb> p.next
=> 3
irb> p.next
=> 5
irb> p.next
=> 7
irb> p.take(3)
=> [2, 3, 5]
irb> p.first(3)
=> [2, 3, 5]
irb> p.next
=> 11
Note how the last p.next
has continue the list showed before in the last call to p.next
. But in the middle, we called p.take
and p.first
and the index of the state was not altered.
Conclusion
Ruby offers us different ways to do the same. In this case, the use of Enumerable is the best approach.
I have been playing with Fiber
and Proc
to achieve similar results, but their purpose are not to resolve this kind of issues. Also, I investigate about Fiber
and I found that Matz proposed to include the Enumerable
to Fiber
by default. The proposal was finally rejected because they didn’t find any reasonable use case.