Understanding the Object Model

Camilo Reyes
Share

Structure and hierarchy background

Have you ever wondered why Ruby behaves like this?

class A
  def self.my_method
    'hello'
  end
  def my_other_method
    'hello again'
  end
end

A.my_method # => "hello"

a = A.new
a.my_other_method # => "hello again"

a.my_method # => NoMethodError: undefined method

If you come from a classical language such as Java, you may consider that my_method is much like a static method. But, there are key differences:

  • Class methods get invoked only from the class and get inherited.
  • Instance methods get invoked only through an instance of the class and get inherited.
  • Instance objects can also declare “class” methods.

Confused yet?

In this article, we’ll explore the object model. This will give you a deeper insight into the way Ruby stacks and manages objects. For the purposes of this article, we’ll be using Ruby MRI 2.1. If interested, I recommend firing up irb in a console and getting your feet wet. Don’t forget to wear floaties as we’ll be going to the deep end of the pool. I’ll have mine on, too.

What is a Singleton Method?

What if I told you that every method in Ruby is a singleton method? If what I am saying to you is true, then every singleton method must belong to a “class” singleton object. Now, if you know the singleton design pattern in a classical language, forget everything you have learned. If you have never heard of singleton methods, then you are already one step ahead. Let’s have a look:

class D; def method_one; 'hello from method_one'; end; end
def D.method_two; 'hello from method_two'; end
D.method_two
# => "hello from method_two"
d = D.new
d.method_one
# => "hello from method_one"

From Ruby’s perspective, these two method invocations would be similar. In theory, what we would have are singleton methods of a singleton object invoked. So, when the receiver D or d invokes a method, there must be a lookup in Ruby that goes one step right to the “class” and up the ancestors chain. But wait, you say, isn’t one an instance method and one a class method?

Let’s take a closer look and see how deep this rabbit hole goes:

d.class
# => D
d.class.instance_methods.grep(/method_one/)
# => [:method_one]

So far, so good. The method lookup in receiver d is going one step right to the “class” and finding method_one. Keep in mind that, in Ruby, classes are singleton objects, too. But, what about D?

D.class
# => Class
D.class.instance_methods.grep(/method_two/)
# => []

Wait a second, something is amiss. My method is not defined in Class so there must be more to this than meets the eye.

Well, something doesn’t feel quite right so how about this:

def d.method_three; 'hello from method_three'; end;
d.method_three
# => "hello from method_three"
d.class
# => D
d.class.instance_methods.grep(/method_three/)
# => []

Wait another second! method_three is not defined in class D, either. Maybe there is something critical we are not seeing from this. One thing is for sure, with these last two invocations, they are both missing from their supposed class. For our singleton method hypothesis to make any sense, these methods must belong to a “class”. Let’s see if we can find these elusive classes.

Enter Eigenclasses

In Ruby, the terminology seems to be at odds on what to call these metaclasses. Yukihiro “Matz” Matsumoto hasn’t announced an official name yet. But he seems to like the mathematician-friendly name of eigenclass. Another accepted term is singleton class.

The word eigen in German (pronounced: AYE-gun), roughly means “one’s own”. In this case, meaning an object’s “own class.” In this article, we’ll stick with eigenclass for our terminology.

To find these elusive UFOs in Ruby, you can do:

d.singleton_class
# => #<Class:#<D:0x007ff882b629c0>>

Or, if you want to impress your friends with your mad Ruby skills, hack up:

class Object; def eigenclass; class << self; self; end; end; end
d.eigenclass
# => #<Class:#<D:0x007ff882b629c0>>

With this code, we are reopening Object, which is up in the ancestors chain (more on this later) and adding a method. This method goes right into the eigenclass and returns “self” which is the eigenclass of the receiver. Neat trick, huh?

Now that we have found our eigenclasses, let’s check to see if my theory makes any sense:

D.eigenclass.instance_methods.grep(/method_two/)
# => [:method_two]
d.eigenclass.instance_methods.grep(/method_three/)
# => [:method_three]

Tada! We have found our methods. Notice how this method lookup is not any different from the lookup on method_one. To recap, it is one step right to the “class” and up the ancestors chain. Ruby seems to wrap this code for us in a lookup method called singleton_methods, but now we know it is all smoke and mirrors.

I should mention a word on duck-typing, since some folks get horrified by singleton methods in Ruby. Duck typing is: “If it walks like a duck and quacks like duck, then it must be a duck.” In Ruby, the “type” of an object is a set of singleton methods to which it responds. It is not bound to a class definition.

I know, you might be hearing Spiderman’s Uncle Ben in your head: “With great power, comes great responsibility.” Ruby always assumes that you are responsible in wielding this powerful technique.

Method Lookup in Action

Armed with our sure knowledge of Ruby, we should be able to paddle our way towards the deep end. Let’s draft a quick “lab rat” program so we can verify the object model at work:

class C; def an_instance_method; end; end
def C.a_class_method; end
class E < C; end
obj = E.new
def obj.a_singleton_method; end

Now, let’s go one step right to the “class” and up the ancestors chain to see how everything arranges:

arr = [:an_instance_method, :a_class_method, :a_singleton_method]
obj.eigenclass
# => #<Class:#<E:0x007f8b2a85cd40>>
obj.eigenclass.instance_methods.select { |m| arr.include?(m) }
# => [:a_singleton_method, :an_instance_method]
obj.eigenclass.superclass
# => E
obj.class
# => E
E.superclass
# => C
C.instance_methods.select { |m| arr.include?(m) }
# => [:an_instance_method]
C.superclass
# => Object
Object.superclass
# => BasicObject
BasicObject.superclass
# => nil

An important thing to note is the superclass of an eigenclass of an object is the object’s class. I recommend repeating that last sentence over and over until everything just clicks. The ancestors chain stays on the classes which is why we can find the methods inside obj’s eigenclass. As shown, :an_instance_method belongs to C. Also, instance_methods pulls methods from the ancestors chain as well.

What about the eigenclasess of our classes?

E.eigenclass
# => #<Class:E>
E.eigenclass.superclass
# => #<Class:C>
C.eigenclass.instance_methods.select { |m| arr.include?(m) }
# => [:a_class_method]
C.eigenclass.superclass
# => #<Class:Object>
Object.eigenclass.superclass
# => #<Class:BasicObject>
BasicObject.eigenclass.superclass
# => Class
Class.superclass
# => Module
Module.superclass
# => Object

Looking at the above example, the superclass of an eigenclass of a class is the eigenclass of the class’s superclass. Now, repeat that last sentence, super-fast! The ancestors chain for classes and eigenclasses merge at Object. See, it is not so tough to swim with the “big boys” after all.

But, What About Modules?

Turns out, in Ruby modules aren’t much different than classes. The key difference being that they don’t show up in the ancestors chain when you type up superclass. Let’s see if there is a way to track modules, try:

module F; def a_module_method; end; end
class E; include F; end
E.superclass
# => C
E.ancestors
# => [E, F, C, Object, Kernel, BasicObject]

As you can see, when you add a module it gets appended to the ancestors chain. We can verify this with:

E.instance_methods.include?(:a_module_method)
# => true

In Conclusion

Okay, it is now time for a Pop Quiz!

Can you tell me what happens when I do this?

module F; class G; end; end

How does that look like in the ancestors chain? Is there a way to instantiate class G? If you get completely lost, leave comments.

The Ruby object model is one that is mysteriously captivating and simple. Once you wrap your head around it, the entire language feels like it falls right into place. I hope that you have now gained more insight into mastering this beautiful language.

Happy hacking!

CSS Master, 3rd Edition