What are the benefits of using Deferrable instead of callback. A 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 two levels of nesting, while callbacks have three of them.
As I see it, with a Deferrable object, you can, for example, pass errback and define only a callback. Which makes sense in some cases, so the code becomes more readable with fewer lines of code and less nesting.
But. I found one unpleasant case. For example, you have this fake asynchronous 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
What it should look like 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 little cleaner (to my taste). But this appreciation is subjective. Imagine that you are going to support a synchronous API through your asynchronous one. With the Deferrable Interface, you should wrap all your methods with deferred answers in Fiber (which is a lot of work and very hard to support), while in a callback you should 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 just wrapped your socket in Fiber, and your asynchronous code has become synchronous. Thrally said that you should also override 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
These are minor changes, but in a few lines of code your API can work in both synchronization and async modes.
Sorry for such a great introduction and thanks for reading (all of you two guys).
Question. Disabling is the de facto standard api event in Ruby. So maybe I donβt understand something, and am I using Deferrable incorrectly? Perhaps the callback interfaces smell and have some bad issues?
PS: I wrote all this because I'm working on the MongoDB driver in EventMachine and now I am adding a synchronous interface to the client. And in the end I realized that I need to decapitate all public APIs to add support for synchronization due to Deferrables and think about rewriting it for callbacks.