1. Computing

Dynamic Method Calls

By

Dynamic Method Calls

If you're a C#, C++ or Java programmer, you've probably gotten used to some kind of early binding for method calls. All of those languages have some sort of mechanism to know the type of an object and find the correct method call quickly and simply call it. Underneath it all, the methods might as well be functions that are almost called directly by memory address. But things are different in Ruby.

Compile Time Vs. Run Time

Remember that Ruby has very little concept of a "compile time." The compile stage doesn't do anything but parse the Ruby code into a syntax tree, bytecode or similar. It doesn't actually try to interpret anything, it doesn't keep special tables of what classes have which method, etc. All of that lives with the class itself, with its list of methods.

To add to the disconnect between the two types of Object Oriented languages (the "fast" ones like C++ and the "pure" ones like Ruby), method calls are more like Smalltalk's messages. There is no absolute list of which objects or which types respond to a particular message, you just send it and if it responds, it responds. If it doesn't, an exception is raised.

For this reason, when Ruby calls a method on an object, it doesn't have any fast lookup tables. Yes, this is slower than the "fast" method, but it's slower by design. Methods are often added or removed at runtime, and objects can even respond to messages even if they don't have a method defined with that name.

Responding to Messages

Ruby objects are sent messages when either the method call operator (the familiar dot operator) is used on them, or their send method is called. Both do almost the same exact thing, but the send method's argument can be a string, so messages can be passed programatically.

Ruby objects respond to messages in a few ways.

  • Methods - If you've defined a method with a matching name in that class, that method will be called. This is the most familiar mechanism in this list, it's just a method call.
  • Inherited Methods - In addition to the object's class' methods, all of the methods from the ancestor chain are searched. This is very similar to how inheritance works in virtually any inheritance-based (as opposed to prototype-based) Object Oriented language.
  • Mixin Module Methods - Related to inherited methods, methods from mixin modules are searched. While it doesn't seem related at first (composition vs. inheritance), mixin modules insert themselves into the ancestor chain and act like parent classes.
  • Method Missing - As a last resort, Ruby will see if the object will respond to the message using method_missing. This is perhaps the most bizarre method if you're coming from a more traditional language: Ruby objects can respond to messages even if they don't have any methods defined with that name.

Sending Messages

In addition to responding to messages dynamically, Ruby can also send messages dynamically. By using the send method, whose first parameter is a string or symbol with the method name and remaining parameters are passed to the method if the object responds, you can send any message. This includes messages based on user input, hash tables, etc.

Sending messages with the send method is a very powerful way to make dynamic interfaces. The following code implements a simple REPL (read, eval, print, loop) that sends messages to a single object based on user input. Note that similar code can be seen in production code, but it will often by sanitized first. Since the send method responds to anything in the inheritance chain, methods from Object, BasicObject and Kernel can be called by the user. Depending in which context the program is run in, this could be a security risk or, at the very least, could accidentally break the program.


#!/usr/bin/env ruby

class Player
  def initialize
    @loc = {x: 0, y: 0}
  end

  def report
    puts "I am at #{@loc[:x]},#{@loc[:y]}"
  end

  def north
    @loc[:y] -= 1
    report
  end

  def south
    @loc[:y] += 1
    report
  end

  def east
    @loc[:x] += 1
    report
  end

  def west
    @loc[:x] -= 1
    report
  end
end

p = Player.new

loop do
  print "? "
  command = gets.chomp

  if command == 'quit'
    break
  else
    p.send(command)
  end
end

As can be see in the code, the player can move north, south, east and west. The REPL loop at the bottom reads input from the user and sends it to the player.

Refactor: Use method_missing

That last example was a bit verbose. All of the methods just get repeated with slight modifications. This is not good. We can leverage a Ruby's method_missing and a lookup hash to simplify this considerably.


#!/usr/bin/env ruby

class Player
  @@directions = {
    north: {y: -1},
    south: {y:  1},
    east:  {x:  1},
    west:  {x: -1}
  }

  def initialize
    @loc = {x: 0, y: 0}
  end

  def report
    puts "I am at #{@loc[:x]},#{@loc[:y]}"
  end

  def method_missing(m, *args)
    if @@directions.has_key?(m)
      @@directions[m].keys.each do|k|
        @loc[k] += @@directions[m][k]
      end
      report
    else
      super
    end
  end
end

p = Player.new

loop do
  print "? "
  command = gets.chomp

  if command == 'quit'
    break
  else
    p.send(command)
  end
end

The only changes of note here are the addition of the lookup hash class variable @@directions and the method_missing method. If the method called is in the @@directions table, look up its values and apply them to the player's location. Otherwise, call super. The super part is important, otherwise this method_missing method will eat all invalid method calls and no exceptions will be raised.

This was pretty good, but there's another way that's perhaps a bit more clean.

Defining Methods Dynamically

Instead of relying on method_missing we can define our methods dynamically when the class is defined. Remember that Ruby makes no real distinction between compile time and run-time. Ruby class definitions are not special in any way, they're just code that usually only defines some methods. But you can run code in your class definitions (in fact, this is how the attr_* methods work), and we're going to do that to dynamically define our methods from the @@directions lookup table.

But first, why? If the method_missing solution worked, why do this? There are a few reasons, first and foremost being that method_missing is what happens when Ruby is out of options. You can do some clever things with it, but it's not the only way out there. It also gives no feedback to the user. Someone trying to figure out how to use your object in IRB will probably list its methods. Since none of the "methods" implemented with methods_missing show up in these lists, it will just confuse things further. And finally, since more steps are involved in calling the method, it's slow.


#!/usr/bin/env ruby

class Player
  @@directions = {
    north: {y: -1},
    south: {y:  1},
    east:  {x:  1},
    west:  {x: -1}
  }

  @@directions.keys.each do|d|
    define_method(d) do
      @@directions[d].keys.each do|k|
        @loc[k] += @@directions[d][k]
      end
      report
    end
  end

  def initialize
    @loc = {x: 0, y: 0}
  end

  def report
    puts "I am at #{@loc[:x]},#{@loc[:y]}"
  end
end

p = Player.new

puts "The player can: #{p.class.instance_methods(false)}"
loop do
  print "? "
  command = gets.chomp

  if command == 'quit'
    break
  else
    p.send(command)
  end
end

For the most part, this code is the same. The same logic is used, it's just moved from a method_missing into a define_method. Also note the line at the beginning of the REPL. Since the methods are actually defined now, we can list them at the beginning of the loop. This is probably a more sound way of implementing dynamic methods, but it does have one weakness: you have to define all the methods. That might sound obvious, but your object may respond to different methods depending on its state. For example, it might respond to "jump" if it's in the "standing" state, but not when it's in the "dead" state. But in the end, which technique to use is up to you. There is no one right way to do this.

  1. About.com
  2. Computing
  3. Ruby
  4. Beginning Ruby
  5. Object Oriented Programming
  6. Dynamic Method Calls

©2014 About.com. All rights reserved.