Unexpected Result When Using Fibers

I wrote a simple ruby fiber program to see how they work and got an unexpected result.

#! /usr/bin/env ruby
#encoding: utf-8
#frozen_string_literal: true

sg = Fiber.new do
  File.open(begin print "Enter filename: "; gets.chomp end).each{|l| Fiber.yield l}
end

begin
  loop do
    puts sg.resume
  end
rescue => err
  puts "Error: #{err.message}"
end

datafile

This is the first
This is the second
This is the third
This is the fourth
This is the fifth

And the output from the above program

Enter filename: datafile
This is the first
This is the second
This is the third
This is the fourth
This is the fifth
#<File:0x0000557ce26ce3c8>
Error: attempt to resume a terminated fiber

I’m not sure why its displaying #File:0x0000557ce26ce3c8 in the output.

Note: ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-gnu]

>Solution :

From the docs,

Upon yielding or termination the Fiber returns the value of the last executed expression

In addition to Fiber.yield calls, a fiber (like an ordinary function) returns the result of the final expression in the fiber.

The body of your fiber is this.

File.open(begin print "Enter filename: "; gets.chomp end).each{|l| Fiber.yield l}

Inside the .each, you yield each line of the file, which gets printed out as you’ve already observed. But then, when the fiber is done, it yields a final value, which is the result of File.open. And File.open returns the File object itself. So your sg.resume actually sees six results, not five.

"This is the first"
"This is the second"
"This is the third"
"This is the fourth"
"This is the fifth"
(the file object itself)

This actually points out a small issue in your program to begin with: You never close the file. In order to be completely safe, your fiber code should probably look like this.

sg = Fiber.new do
  print "Enter filename: "
  filename = gets.chomp
  # By passing File.open a block, the file is closed after the block automatically.
  File.open(filename) do |f|
    f.each{|l| Fiber.yield l}
  end
  # Our fiber has to return something at the end, so let's just return nil
  nil
end

begin
  loop do
    # Get the value. If it's nil, then we're done; we can break out of the loop.
    # Otherwise, print it
    value = sg.resume
    break if value.nil?
    puts value
  end
rescue => err
  puts "Error: #{err.message}"
end

Now, in addition to dealing with that pesky file handle, we have a way to detect when the fiber is done and we no longer get the "attempt to resume a terminated fiber" error.

Leave a Reply