fl00r
fl00r

Reputation: 83680

Deferrable vs callback interface

What are benefits of using Deferrable instead of callbacks. Small example.

# This is Deferrable interface
request = NetworkIO.get
request.callback do |resp|
  puts "Job is done"
end
request.errback do |err|
  puts "Oh. My name is Forest, Forest Gump"
end

# And this is callback interface
NetworkIO.get do |resp|
  if Exception === resp
    puts "Oh. My name is Forest, Forest Gump"
  else
    puts "Job is done!"
  end
end

# Or node js style
NetworkIO.get do |err, resp|
  if err
    puts "Oh. My name is Forest, Forest Gump"
  else
    puts "Job is done!"
  end
end

Deferrable has got two nesting levels, while callbacks has got three of them.

As I can see, with Deferrable object you can pass errback, for example, and define only callback. Which is have sense in some cases, so code become more readable with less lines of code and less nesting.

But. I've found one annoying case. For example you have got this fake async API.

class SomeIO
  def find(msg)
    response = DeferrableResponse.new
    req = socket.send(msg)
    req.callback do |data|
      response.succeed(data)
    end
    req.errback do |err|
      response.fail(err)
    end
    response
  end

  def count(msg)
    response = DeferrableResponse.new
    req = find(msg)
    req.callback do |data|
      response.succeed(data.size)
    end
    req.errback do |err|
      response.fail(err)
    end
    response
  end
end

How should it looks in callback mode

class SomeIO
  def find(msg, &blk)
    socket.send(msg) do |resp|
      blk.call(resp)
    end
  end

  def count(msg, &blk)
    find(msg) do |resp|
      if Exception === resp
        blk.call(resp)
      else
        cnt = resp.size
        blk.call(cnt)
      end
    end
  end
end

Even now it looks a bit cleaner (for my taste). But it is appreciation subjective. Imagine, that you are going to support synchronouse API over your asynchronouse one. With Deferrable Interface you should wrap all your methods with deferrable responses into Fiber (which is very big work and very heavy to support), while in callback you have to call Fibers only on trully async ops:

class SyncSomeIO < SomeIO
  def find(msg, &blk)
    fib = Fiber.current
    socket.send(msg) do |resp|
      fib.resume(resp)
    end
    res = Fiber.yield
    raise res if res === Exception
    res
  end
end

So you've just wrapped your socket in Fiber and your async code became synchronouse. Trully to said you should also redefine all your block methods:

class SomeIO
  ...

  def count(msg, &blk)
    find(msg) do |resp|
      if Exception === resp
        block_given? ? blk.call(resp) : resp
      else
        cnt = resp.size
        block_given? ? blk.call(cnt) : cnt
      end
    end
  end
end

It is minor change, but in just few lines of code your API could work both in sync and async modes.

Sorry for such a big introduction and thank you for reading (all of you two guys).

The question is. Deferrable is de-facto is a standard of evented api in Ruby. So maybe I misunderstanding something, and I use Deferrable wrong way? Maybe callback interfaces smells and have got some bad issues?

PS: I wrote all this because I am working on MongoDB driver on EventMachine and now adding synchronouse interface to client. And eventually realized that I should monkey patch all public API to add sync support because of Deferrables and thinking about to rewrite it on callbacks.

Upvotes: 4

Views: 378

Answers (1)

Jacob
Jacob

Reputation: 23225

If you control the Deferrable's interface, you could do something along the lines of:

class DeferrableResponse
  def sync
    fiber = Fiber.current

    callback { |val| fiber.resume(val) }
    errback { |err| fiber.resume(err) }

    result = Fiber.yield
    raise result if result.is_a? Exception
    result
  end
end

This will let you use the synchronous API like this:

response = NeworkIO.get.sync

Upvotes: 1

Related Questions