28 Dec, 2008 in Ruby by khelll

Ruby callbacks

This blog post is about ruby’s callbacks(hooks): what are the available ones,and how practically we can use them?

method_missing

obj.method_missing(symbol [, *args] ) => result might be the most famous hook in ruby, and is being used a lot by ruby developers :

Invoked by Ruby when obj is sent a message it cannot handle. symbol is the symbol for the method called, and args are any arguments that were passed to it. By default, the interpreter raises an error when this method is called. However, it is possible to override the method to provide more dynamic behavior.

Here is a simple example where the user can mix colors using the mix method :

class Colors 
  def mix(*colors)
    if block_given?
      yield(colors)
    else
      puts "Mixing: #{colors.join ' + '}"
    end
  end
end
c = Colors.new
c.mix "orange" , "cyan" #=> Mixing: orange + cyan
c.mix("green","red"){|colors| puts "A nice mix: #{colors.join(' + ')}"} #=> A nice mix: green + red

Now, how about letting the user do something like: ‘c.greenAndBlue’ or ‘c.grayAndYellow’ ?, let’s see how to do so:

class Colors 
  def mix(*colors)
    if block_given?
      yield(colors)
    else
      puts "Mixing: #{colors.join ' + '}"
    end
  end
 
  def method_missing(*args, &block)
    # First arg is a symbol representing the name of the method.
    method_name = args.shift
    # Extract the colors from the method name and collecting them in an array 
    # This is a very basic treatment.
    colors = method_name.id2name.split(/and/i).collect{|color| color.downcase}
    # Send the colors and the block to 'mix' method.
    self.send :mix, colors ,&block
  end
end
 
c = Colors.new
c.mix "orange" , "cyan" #=> Mixing: orange + cyan
c.mix("green","red"){|colors| puts "A nice mix: #{colors.join(' + ')}"} #=> A nice mix: green + red
c.greenAndBlue #=> Mixing: green + blue 
c.greenAndWhiteAndBrown #=> Mixing: green + white + brown
c.grayAndYellow{|colors| puts "A nice mix: #{colors.join(' + ')}"} #=> A nice mix: gray + yellow

const_missing

const_missing : invoked when a reference is made to an undefined constant.

I will use it to show the user a more informative message when he tries to use a non existing constant:

class Colors
	RED = "#FF0000"
	GREEN = "#00FF00"
	BLUE = "#0000FF"
 
	def self.const_missing(name)
		puts "Sorry the color '#{name}' is undefined; #{constants.join ","} are the only available colors."
	end
end
 
Colors::Yellow #=> Sorry the color 'Yellow' is undefined; BLUE,GREEN,RED are the only available colors.

included and extended

included and extended are fired when the module is included or extended :

module A
	def self.included(base)
		puts "#{self} included in #{base}"
	end
 
	def self.extended(base)
		puts "#{self} extended in #{base}"
	end
end
 
class B
	include A #=> A included in B
end
 
class C
	extend A #=> A extended in C
end

However let’s move to a more practical use, if u look at the module documentation, you will find the following definition:

A Module is a collection of methods and constants. The methods in a module may be instance methods or module methods. Instance methods appear as methods in a class when the module is included, module methods do not.

Well, as you can see, you can’t include the module methods inside your class, then what to do?
I saw some people use a nice trick to do so, they split their module into 2 inner modules, one called InstanceMethods and the other called SingletonMethods, then they include the former, and extend the later, look at the following snippet of code :

module Greetings
  # When a class is including this module
  def self.included(base)
    # include: will mix the methods in the caller as instance ones.
    # include: is a private method that's why we are using 'send'
    base.send :include, InstanceMethods
    # extend: will mix the methods in the caller as class ones.
    # extend: is a private method that's why we are using 'send'
    base.send :extend, SingletonMethods
  end
 
  # Put all the the methods that should be mixed as instance ones here.
  module InstanceMethods
    def hello ; puts "hello" ; end
  end
 
  # Put all the the methods that should be mixed as class ones here.
  module SingletonMethods
    def bye ; puts "bye!" ; end
  end
end
 
class Greeter
  # The 'included' callback is getting fired now 
  include Greetings
end
 
Greeter.new.hello #=>hello
Greeter.bye #=> bye!

method_added and singleton_method_added

method_added and singleton_method_added are another 2 callbacks that are fired when a new instance or singleton method is added.
In the code snippet bellow, I’m trying to prevent a developer from monkey patching(reopen) my colors class:

class Colors
  def self.method_added(name)    
    puts "A trial to add a new instance method: #{name}, this is not allowed." 
    undef_method name
  end 
 
  def self.singleton_method_added(name)   
    if name != :singleton_method_added
      singleton_class = class << self; self; end
      puts "A trial to add a new class method: #{name}, this is not allowed." 
      singleton_class.class_eval{undef_method name}
    end
  end 
end
 
#Monkey patching 
class Colors
  def red ; end #A trial to add a new instance method: red, this is not allowed.
  def self.mixer ; end #A trial to add a new class method: mixer, this is not allowed.
end

method_removed and method_undefined

method_removed: fired when an instance method is removed.
method_undefined: fired when an instance method is undefined.

singleton_method_removed and singleton_method_undefined

singleton_method_removed: fired when a singleton method is removed.
singleton_method_undefined: fired when a singleton method is undefined.

inherited

inherited is called when some class is being inherited:

class Colors
  def self.inherited(sub)
    puts "class #{sub} is inheriting class #{self}"
  end
end
 
class C < Colors ; end #=> class C is inheriting class Colors

A practical use will be in preventing subclasses from being created :

class Colors
  def self.inherited(sub) 
    raise "class #{sub} is inheriting class #{self}, inheritance is now allowed...."
  end
end
 
class D < Colors ; end #=> class D is inheriting class Colors, inheritance is now allowed.... (RuntimeError)

Well, that’s all for this post. The next post series will be on writing internal DSLs using ruby.
See you then…..

6 Responses so far | Have Your Say!

  1. Mathieu - Gravatar

    Mathieu  |  December 28th, 2008 at 11:28 am #

    Khaled, I just discovered your blog and you are writing amazing technical posts. Great job and thanks for helping the Ruby community!

  2. khelll - Gravatar

    khelll  |  December 28th, 2008 at 11:30 am #

    Thanks @ Mathieu

  3. Phani - Gravatar

    Phani  |  December 29th, 2008 at 9:27 pm #

    Hi, nice post and neat clarity about what you are conveying. Keep up the good work and as Mathieu point out, thanks for helping the community! Looking forward to your upcoming posts on Ruby.

  4. José Valim - Gravatar

    José Valim  |  February 16th, 2009 at 7:45 am #

    Great post! :)

    I found it because I’m searching for an after_initialize class callback in Ruby. In other words, it should be fired after loading a class, any idea?

    Keep it up!

  5. billy - Gravatar

    billy  |  February 6th, 2010 at 9:12 am #

    Why did you use “self.send :mix, colors ,&block” instead of calling the function “normaly”.

    btw, thanks for the nice post :)

  6. khelll - Gravatar

    khelll  |  February 6th, 2010 at 9:31 am #

    @billy, cause mix is a Symbol there. It can’t be called directly. Another note, you can’t pass methods directly just like a one can do in Python or Javascript and then call it directly. The maximum u can do is to pass the name(symbol or string) and call it via send or eval methods, Or pass method object and call it from there.
    Anyway check this post on dynamic method invocation in Ruby.

Leave a Feedback

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="">