How to implement a "callback" in Ruby?
Explored the topic a bit more and updated the code.
The following version is an attempt to generalize the technique, although remaining extremely simplified and incomplete.
I largely stole - hem, found inspiration in - the implementation of callbacks of DataMapper, which seems to me quite complete and beatiful.
I strongly suggest to have a look at the code @ http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb
Anyway, trying to reproduce the functionality using the Observable module was quite engaging and instructive. A few notes:
- method added seems to be require because the original instance methods are not available at the moment of registering the callbacks
- the including class is made both observed and self-observer
- the example is limited to the instance methods, does not support blocks, args and so on
code:
require 'observer'
module SuperSimpleCallbacks
include Observable
def self.included(klass)
klass.extend ClassMethods
klass.initialize_included_features
end
# the observed is made also observer
def initialize
add_observer(self)
end
# TODO: dry
def update(method_name, callback_type) # hook for the observer
case callback_type
when :before then self.class.callbacks[:before][method_name.to_sym].each{|callback| send callback}
when :after then self.class.callbacks[:after][method_name.to_sym].each{|callback| send callback}
end
end
module ClassMethods
def initialize_included_features
@callbacks = Hash.new
@callbacks[:before] = Hash.new{|h,k| h[k] = []}
@callbacks[:after] = @callbacks[:before].clone
class << self
attr_accessor :callbacks
end
end
def method_added(method)
redefine_method(method) if is_a_callback?(method)
end
def is_a_callback?(method)
registered_methods.include?(method)
end
def registered_methods
callbacks.values.map(&:keys).flatten.uniq
end
def store_callbacks(type, method_name, *callback_methods)
callbacks[type.to_sym][method_name.to_sym] += callback_methods.flatten.map(&:to_sym)
end
def before(original_method, *callbacks)
store_callbacks(:before, original_method, *callbacks)
end
def after(original_method, *callbacks)
store_callbacks(:after, original_method, *callbacks)
end
def objectify_and_remove_method(method)
if method_defined?(method.to_sym)
original = instance_method(method.to_sym)
remove_method(method.to_sym)
original
else
nil
end
end
def redefine_method(original_method)
original = objectify_and_remove_method(original_method)
mod = Module.new
mod.class_eval do
define_method(original_method.to_sym) do
changed; notify_observers(original_method, :before)
original.bind(self).call if original
changed; notify_observers(original_method, :after)
end
end
include mod
end
end
end
class MyObservedHouse
include SuperSimpleCallbacks
before :party, [:walk_dinosaure, :prepare, :just_idle]
after :party, [:just_idle, :keep_house, :walk_dinosaure]
before :home_office, [:just_idle, :prepare, :just_idle]
after :home_office, [:just_idle, :walk_dinosaure, :just_idle]
before :second_level, [:party]
def home_office
puts "learning and working with ruby...".upcase
end
def party
puts "having party...".upcase
end
def just_idle
puts "...."
end
def prepare
puts "preparing snacks..."
end
def keep_house
puts "house keeping..."
end
def walk_dinosaure
puts "walking the dinosaure..."
end
def second_level
puts "second level..."
end
end
MyObservedHouse.new.tap do |house|
puts "-------------------------"
puts "-- about calling party --"
puts "-------------------------"
house.party
puts "-------------------------------"
puts "-- about calling home_office --"
puts "-------------------------------"
house.home_office
puts "--------------------------------"
puts "-- about calling second_level --"
puts "--------------------------------"
house.second_level
end
# => ...
# -------------------------
# -- about calling party --
# -------------------------
# walking the dinosaure...
# preparing snacks...
# ....
# HAVING PARTY...
# ....
# house keeping...
# walking the dinosaure...
# -------------------------------
# -- about calling home_office --
# -------------------------------
# ....
# preparing snacks...
# ....
# LEARNING AND WORKING WITH RUBY...
# ....
# walking the dinosaure...
# ....
# --------------------------------
# -- about calling second_level --
# --------------------------------
# walking the dinosaure...
# preparing snacks...
# ....
# HAVING PARTY...
# ....
# house keeping...
# walking the dinosaure...
# second level...
This simple presentation of the use of Observable could be useful: http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html
The ruby equivalent, which isn't idiomatic, would be:
def my_callback(a, b, c, status_code)
puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
end
def do_stuff(a, b, c, callback)
sum = a + b + c
callback.call(a, b, c, sum)
end
def main
a = 1
b = 2
c = 3
do_stuff(a, b, c, method(:my_callback))
end
The idiomatic approach would be to pass a block instead of a reference to a method. One advantage a block has over a freestanding method is context - a block is a closure, so it can refer to variables from the scope in which it was declared. This cuts down on the number of parameters do_stuff needs to pass to the callback. For instance:
def do_stuff(a, b, c, &block)
sum = a + b + c
yield sum
end
def main
a = 1
b = 2
c = 3
do_stuff(a, b, c) { |status_code|
puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
}
end
This "idiomatic block" is a very core part of everyday Ruby and is covered frequently in books and tutorials. The Ruby information section provides links to useful [online] learning resources.
The idiomatic way is to use a block:
def x(z)
yield z # perhaps used in conjunction with #block_given?
end
x(3) {|y| y*y} # => 9
Or perhaps converted to a Proc; here I show that the "block", converted to a Proc implicitly with &block
, is just another "callable" value:
def x(z, &block)
callback = block
callback.call(z)
end
# look familiar?
x(4) {|y| y * y} # => 16
(Only use the above form to save the block-now-Proc for later use or in other special cases as it adds overhead and syntax noise.)
However, a lambda can be use just as easily (but this is not idiomatic):
def x(z,fn)
fn.call(z)
end
# just use a lambda (closure)
x(5, lambda {|y| y * y}) # => 25
While the above approaches can all wrap "calling a method" as they create closures, bound Methods can also be treated as first-class callable objects:
class A
def b(z)
z*z
end
end
callable = A.new.method(:b)
callable.call(6) # => 36
# and since it's just a value...
def x(z,fn)
fn.call(z)
end
x(7, callable) # => 49
In addition, sometimes it's useful to use the #send
method (in particular if a method is known by name). Here it saves an intermediate Method object that was created in the last example; Ruby is a message-passing system:
# Using A from previous
def x(z, a):
a.__send__(:b, z)
end
x(8, A.new) # => 64
Happy coding!