Pregunta

I don't understand how the below:

counts = Hash.new(0)

File.foreach("testfile") do |line|
  line.scan(/\w+/) do |word|
    word = word.downcase
    counts[word] += 1
  end
end

counts.keys.sort.each {|k| print "#{k}:#{counts[k]} "}

Is so much worse than:

words = Fiber.new do
  File.foreach("testfile") do |line|
    line.scan(/\w+/) do |word|
      Fiber.yield word.downcase
    end
  end
end

counts = Hash.new(0)

while word = words.resume
  counts[word] += 1
end

counts.keys.sort.each {|k| print "#{k}:#{counts[k]} "}
¿Fue útil?

Solución

Fibers are a way of suspending and resuming arbitrary blocks of code. An example like this isn't really a great use-case as it doesn't offer any real advantage over the traditional way of reading in lines and processing them.

In this particular example, if you wanted to make it better, you'd write an enumerator-style interface so you could write:

words = WordsReader.new("testfile")

words.each do |word|
  # ...
end

Where Fibers become important is in writing asynchronous code. For example, inside the EventMachine environment you need to be able to issue an asynchronous call, suspend a block of code, and resume it when you receive the response.

This ends up looking like this:

async_call(argument1, argument2) do |response_1|
  if (response_1.ok?)
    async_call(argument3, argument4) do |response_2|
      if (response_2.ok?)
        async_call(argument5, argument6) do |response_3|
          if (response_3.ok?)
            do_something(response_1, response_2, response_3)
          else
            panic_and_fail!
          end
        end
      else
        panic_and_fail!
      end
    end
  else
    panic_and_fail!
  end
end

This sort of nested, nested and re-nested call structure is loosely termed "callback hell" as it gets very difficult to manage once your logic becomes non-trivial. One way to flatten this structure is to employ Fibers. A properly Fiber-ized equivalent is:

begin
  response_1 = fiber_call(argument1, argument2)
  response_2 = fiber_call(argument3, argument4)
  response_3 = fiber_call(argument5, argument6)

  do_something(response_1, response_2, response_3)

rescue NotOkay
  panic_and_fail!
end

Fibers can take advantage of exceptions, where callback-type code cannot. Exceptions, when used effectively, can massively simplify a block of code, as you can see here. Instead of testing for ok? on each response, it's expected that the call will throw an exception of type NotOkay instead.

Callbacks cannot reliably throw exceptions since the initiator of the call has already fallen out of scope when the callback occurs. This is a fundamental limitation of asynchronous programming with callbacks. Fiber driven code maintains a proper call stack, it's merely suspended and resumed as-is, so exceptions properly cascade through the caller.

I've found Fibers to be both simple to understand and very difficult to apply correctly. Most of the time you won't have to use them directly, you'll be using a library that employs them instead. Writing "Fiber-aware" code is not unlike writing "Thread-safe" code. It can be tricky to get right.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top