Ruby callbacks
If the Ruby programming language has any single weakness, for me that weakness is how callbacks are defined.
In many programming languages, functions or methods can be declared outside of a specific class, passed around as variables, and invoked just as any other function or method anywhere they’re in scope. For example, Python allows such “top level” functions, and of course in C and C++ functions are just pointers and can be passed around and dereferenced as usual.
Ruby sort of does, and sort of doesn’t have callbacks. For example, it’s easy to define a ruby callback with either a proc or a lambda like so:
foo = -> { 'top level lambda' }
bar = proc { 'top level proc' }
describe 'main' do
it "prints top level foo" do
expect(foo.()).to eq 'top level lambda'
end
it 'prints top level bar' do
expect(bar.()).to eq 'top level proc'
end
end
But this has limited use, as the callback scope has to be controlled explicitly (and only) by file inclusion. Which is fine for C programming, but we’re programming in Ruby.
Also, note that the term “top level” is probably being abused here. For the purpose of the article, “top level” refers to a function-like method defined outside of any enclosing class defined within the script, program or application.
Dispatch table
We can also hash these callbacks to invoke later:
dispatch = {
'foo' => foo,
'bar' => bar
}
describe 'main' do
it 'dereferences dispatch table and invokes 'foo'' do
foo_from_table = dispatch['foo']
expect(foo_from_table.()).to eq 'top level lambda'
end
it 'dereferences dispatch table and invokes 'foo'' do
bar_from_table = dispatch['bar']
expect(bar_from_table.()).to eq 'top level proc'
end
end
That’s all easy enough, and there is no need to set a variable from the
hash dereference, which could be call
‘ed directly. On the other hand,
variables are handy for passing around.
Controlling scope
We can also define constants in a module:
module Foobar
Foo = -> { 'module lambda' }
end
dispatch['foom'] = Foobar::Foo # dispatch defined above
describe 'main' do
it 'dereferences dispatch table and invokes 'Foobar::Foo'' do
foom_from_table = dispatch['foom']
expect(foom_from_table.()).to eq 'module lambda'
end
end
Practical implementation
All the above is well and good, but begs the question “Why would we want to use ruby callbacks?” Here’s one situation, overriding default execution:
#!/usr/bin/env ruby
require 'test/unit'
class CallbackDemo
Default = -> { 'default response' }
def foo &bar
bar ||= Default
bar.call
end
end
`
This implementation was inspired by Avdi Grimm’s Ruby Tapas screencast Caller-specified Fallback Handler. A slightly more in-depth treatment can be found in Avdi Grimm’s Confident Ruby. Also find a more recent treatment of caller-specified callbacks.
Naturally, we want to test it:
class CallbackDemoTest < Test::Unit::TestCase
def test_default_response
assert_equal 'default response', CallbackDemo.new.foo
end
def test_custom_response
assert_equal 'custom response',
CallbackDemo.new.foo { 'custom response' }
end
end
It works with class methods as well:
class CallbackDemo
class << self
def bar &foo
foo ||= Default
foo.call
end
def quux(arg)
arg ? yield(arg) : Default.call
end
end
end
class CallbackDemoTest < Test::Unit::TestCase
def test_class_default_response
assert_equal 'default response', CallbackDemo.bar
end
def test_class_custom_response
assert_equal 'custom response',
CallbackDemo.bar { 'custom response' }
end
def test_argument_passing
# Braced block can be passed to the assertion inline
assert_equal 'foobar response',
CallbackDemo.quux('foobar') { |arg| '#{arg} response' }
# do .. end block cannot be passed inline with the assertion;
# it will produce a `nil` block:
response = CallbackDemo.quux('foobar') do |arg|
'#{arg} response'
end
assert_equal 'foobar response', response
assert_equal 'default response', CallbackDemo.quux(nil)
end
end
Recap
As usual, if I’ve written something unclear, feel free to leave a comment. (or email if comments are turned off.) Likewise if I’ve gotten something wrong. If you’ve written something you believe others would benefit from, feel free to leave a link as well.