Method Dispatch in Ruby
an introduction for .NET Developers
One of the things that makes ruby special is the basic idea that everything is an object. You probably already know that even nil
(the ruby version of null
) is in fact an object, which allows you to do nifty things like simply ask an object if it is nil with .nil?
.
What you might not know is how method dispatch works in the ruby virtual machine. Hopefully by the end of this article you'll have a slightly better idea (no guarantees though!)
What is method dispatch anyway?
First we need to understand what a message is. A message is something that can be sent from one object to another. This can be a name and potentially additional objects as arguments.
account.withdraw 100
In the above example, withdraw
is the message that is being passed to the account
object which is referred to as the receiver. The message contains 100
as the argument.
Method dispatch is the algorithm that is use to work out which method should be invoked in response to a message.
If we take the following code, when withdraw
is called ... how does ruby decide which method to call? That's up to the method dispatcher.
class Account
@balance = 1000
def initialize
@balance = 500
end
def withdraw(pounds)
@balance -= pounds
end
def self.withdraw(pounds)
@balance -= pounds
end
end
account = Account.new
account.withdraw(100) # => 400
Account.withdraw(100) # => 900
There's a few interesting things going on in the code above, but I want to focus on which withdraw
method is being called when.
The key difference between account.withdraw(100)
and Account.withdraw(100)
is that the first is being called on the instance and the second on the class instance - a notion that doesn't exist in the .NET world.
I'd like to quickly point out something I consider a Gotcha! when moving to the ruby world. When you declare a variable inside the class, within .NET you'd expect that to be within the class scope, so all the methods within the class can access it.
public class Account
{
private int balance = 1000;
public int withdraw(int pounds)
{
balance -= pounds
return balance
}
}
/* somewhere else in your code ... */
Account account = new Account()
account.withdraw(100) // => 900
But in ruby, that is very much not the case. If you're as slow as I am, I'd suggest playing around with this a little so it really sinks in.
Namespaces vs. Modules
Ruby doesn't quite have namespaces, but it has modules which are similar ... but far more flexible.
module Declarations
def shennanigans
"SHENNANIGANS!"
end
end
class MakeSomeNoise
include Declarations
end
msn = MakeSomeNoise.new
msn.shennanigans # => "SHENNANIGANS!"
You can see that the include
keyword has pulled in the methods placed inside the module. When you call the method (aka: send the message) shennanigans
to the msn instance, it checks itself for a method by that name. When it fails, it checks the any included modules where in our example it finds the method.
If we try this example again, but include a shennanigans method on the MakeSomeNoise class you can see that it finds it straight away, and therefore doesn't need to check the module.
module Declarations
def shennanigans
"SHENNANIGANS!"
end
end
class MakeSomeNoise
include Declarations
def shennanigans
"Class based shennanigans"
end
end
msn = MakeSomeNoise.new
msn.shennanigans # => "Class based shennanigans"
Ruby, being the ever helpful language that it is will even print out the order that the method dispatcher will follow.
MakeSomeNoise.ancestors # => [MakeSomeNoise, Declarations, Object, Kernel, BasicObject]
If you're using Ruby 2.0 or greater, then you can replace the keyword include
with prepend
to bring in the module before the class.
class MakeSomeNoise
prepend Declarations
def shennanigans
"Class based shennanigans"
end
end
MakeSomeNoise.ancestors # => [Declarations, MakeSomeNoise, Object, Kernel, BasicObject]
extend
ing our knowledge.
We've seen include
and prepend
, but there's a third option called extend
.
If we replace include
with extend, we can see it doesn't appear in our ancestor tree, and if we removed the shennanigans
method from the MakeSomeNoise
class we'd also get a NoMethodError when trying to call it.
class MakeSomeNoise
extend Declarations
def shennanigans
"Class based shennanigans"
end
end
msn = MakeSomeNoise.new
msn.shennanigans # => "Class based shennanigans"
MakeSomeNoise.ancestors # => [MakeSomeNoise, Object, Kernel, BasicObject]
MakeSomeNoise.shennanigans # => "SHENNANIGANS!"
If you look at the last line of the code above, you can see that extend has put the method onto the class instance itself (aka: the class singleton, which has nothing to do with design pattern of the same name). We'll go into a little more detail shortly.
I'm feeling super
So all this is great, but can I still use base
to talk to the method higher in the chain (superclass). Kind of, well.. yes - except it's called super
.
class MakeSomeNoise
include Declarations
def shennanigans
"Class based shennanigans! and #{super}"
end
end
msn = MakeSomeNoise.new
msn.shennanigans # => "Class based shennanigans! and SHENNANIGANS!"
Singleton Classes
I'll only give this a brief mention here, but it's important to remember that because everything is a class even your class has a class. ARGH?! It's a pretty big difference, and you should look into it a bit more... but maybe the following example will make it a bit clearer.
foobar = Array.new
foobar.size # => 0
# add a size method to the singleton class of foobar
def foobar.size
"hello"
end
foobar.size # => "hello"
foobar.class # => Array
barfoo = Array.new
barfoo.size # => 0
What's happening above during the def foobar.size
is that it's adding the size method to the singleton class of foobar, which the method dispatcher will find BEFORE the method on the super class (which is Array). So the order of lookup is actually:
foobar > Singleton (aka: anonymous class) > Array > ...
You can still use the super keyword too, so you can do cool things like:
foobar = [1,2,3,4]
def foobar.size
"Your array has a size of #{super}"
end
foobar.size # => "Your array has a size of 4"
(example taken from here which has more detail and is worth a read)
Object, Kernel and BasicObject
When we looked at the ancestors, we could see that the final three are Object
, Kernel
and BasicObject
.
BasicObject
sits at the very bottom, and is the root class for all of the ruby system. Object
inherits from BasicObject
, and is the default superclass for all user-defined classes. Kernel is actually just a module that contains all of ruby's core methods.
Further Reading
I'm sure there are a great many books on the subject, but if you're after somewhat more instant gratification then I found this blog post pretty interesting.