Turning Ruby block callbacks inside out
Matt Sears wrote a great article about creating callbacks with Ruby blocks
The crux of his method was the building of an anonymous class that would respond correctly to his callbacks
class Proc
def callback(callable, *args)
self === Class.new do
method_name = callable.to_sym
define_method(method_name) { |&block| block.nil? ? true : block.call(*args) }
define_method("#{method_name}?") { true }
def method_missing(method_name, *args, &block) false; end
end.new
end
endFor some reason this post captured my imagination and I started to deconstruct his method, as I couldn’t quite grasp why it worked. What follows is a deconstruction of Matt’s code.
His example involved a small handler for “posting” twitter messages:
def tweet(message, &block)
Twitter.update(message)
block.callback :success
rescue => e
block.callback :failure, e.message
end
tweet "Ruby methods with multiple blocks. #lolruby" do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
endI’m going to work backwards, substituting and expanding this code
until we reach a good position to see what’s happening. We’ll ignore
Matt’s “bonus” style for focus. Firstly, just expand the call to
#tweet, and define the block as a local lambda:
block = lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end
begin
Twitter.update(message)
block.callback :success
rescue => e
block.callback :failure, e.message
endlet’s ignore the rescue clause. In fact we can trim everything
except for one of the block.callback calls. So, going with
:success:
block = lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end
block.callback :successAt this point we need to go back to our original definition of
Proc#callback
class Proc
def callback(callable, *args)
self === Class.new do
method_name = callable.to_sym
define_method(method_name) { |&block| block.nil? ? true : block.call(*args) }
def method_missing(method_name, *args, &block) false; end
end.new
end
end
block = lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end
block.callback :successExpanding out Proc#callback, and realising that in this context,
self is block, callable is :success, and args is []. In
the case of the :failure callback, args would be the parameters
passed to the callback.
block = lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end
block === Class.new do
method_name = :success
define_method(method_name) { |&block| block.nil? ? true : block.call }
def method_missing(method_name, *args, &block) false; end
end.newSimplifying the definition of block as a lambda and the Proc#===
call (Another neat thing that I learnt from Matt’s post), our code
becomes
lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end.call(Class.new do
method_name = :success
define_method(method_name) { |&block| block.nil? ? true : block.call }
def method_missing(method_name, *args, &block) false; end
end.new)Time to simplify the anonymous class definition, let’s get rid of the
error handling in #success too.
lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end.call(Class.new do
def success &block
block.call
end
def method_missing(method_name, *args, &block) false; end
end.new)Hey, now we are getting somewhere! Let’s give the anonymous class a name.
class SuccessCallback
def success &block
block.call
end
def method_missing(method_name, *args, &block)
false
end
end
lambda do |on|
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
end
end.call(SuccessCallback.new)So what has happened? We’ve created a SuccessCallback class that
only responds to #success. And all #success does is call a block
passed to it. Any other method call will absorb any parameters and
any block passed. This is the heart of Matt’s system, as we can see
in the next expansion, where we unwrap the callback lambda.
class SuccessCallback
def success &block
block.call
end
def method_missing(method_name, *args, &block)
false
end
end
on = SuccessCallback.new
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
endAnd it becomes clear from looking at the definition of
SuccessCallback why the callback works. on.success trampolines (to
play fast and loose with the term) to the block passed, and
on.failure falls through to SuccessCallback#method_missing and
thus returns false, ignoring the passed block.
In the :failure case, we would have ended up with:
class FailureCallback
def failure &block
block.call(message) # message is the value of `e.message` in the original code
end
def method_missing(method_name, *args, &block)
false
end
end
on = FailureCallback.new
on.success do
puts "Tweet successful!"
end
on.failure do |status|
puts "Error: #{status}"
endSo, in essence, Matt’s method works by:
-
Defining an anonymous class that responds to a single method,
#successin our example, that calls straight back to a block passed. -
Yielding an instance of this anonymous class to the callback block.
-
The callback block calling every event method on the anonymous instance. However, since the anonymous class ignores all messages bar the event that was “triggered”, the callback only executes the block matching the trigger.
Writing out these steps, I feel a bit sheepish in taking so long to grasp how exactly Matt’s code worked. But what we can do is extract out the concept embodied by the anonymous class, and reformulate the code.
class SavantTrampoline
def initialize method_name, *args
@method_name = method_name.to_sym
@args = args
end
def method_missing method, &block
if method.to_sym == @method_name
yield *@args if block_given?
else
false
end
end
end
class Proc
def callback(callable, *args)
self.call SavantTrampoline.new(callable, *args)
end
endAs a pedagogical exercise, this formulation allows me to more clearly
understand the roles of each actor in this code, although Matt’s
method is more concise. I’m curious whether a class such as
SavantTrampoline (to coin a poor name), would be useful in other
contexts, maybe I should ask Reg
Braithwaite, as his writings on
combinators
has been a big inspiration for me to do this kind of investigation.