Today's class

In Ruby, (almost) everything is an object. Today we’re going to explore how Ruby’s objects can be manipulated and extended at runtime. This is in contrast to objects and classes in languages like Java.

Let’s start with a few quesitons we’d like to answer:

  • What is an object oriented langauge?

  • What is an object?

  • What is a class?

Defining objects

Let’s start by talking about three classes:

 1 class ANumber
 2   def to_s
 3     ""
 4   end
 5 
 6   def isWhole?
 7     false
 8   end
 9   
10   def sum(b)
11     self.add(b)
12   end
13 end
14 
15 class ARational < ANumber
16   attr_accessor :numerator
17   attr_accessor :denominator
18   
19   def to_s
20     "#{@numerator}/#{@denominator}"
21   end
22 
23   def initialize(numerator,denominator=1)
24     @numerator = numerator
25     @denominator = denominator
26   end
27 
28   def add(ratb) 
29     # ...
30   end
31 
32   def isWhole 
33     (@numerator % @denominator) == 0
34   end
35   
36   def toComplex
37     
38   end
39 end
40 
41 class AComplex
42   attr_accessor :real
43   attr_accessor :imaginary
44 
45   def initialize(real, imaginary)
46     @real = real
47     @imaginary = imaginary
48   end
49   
50   def add(complexb)
51     ARational.new(@real+ratb.real,@imaginary+ratb.imaginary)
52   end
53 end

For our purposes, let’s start out by assuming that objects are just clouds. I’ve drawn a Rational object’s cloud here:

Rational

What can we do with these clouds? We can send them messages, and they will return answers to us. For now, let’s assume that a message is a pair consisting of two parts: a symbol (designating the intended method name), and a list of parameters.

Rational

In the above diagram, we see various messages being thrown at (“sent to”) the Rational instance.

But what happens inside the cloud when we call the isWhole? method?

Method resolution and VTables

It turns out that the way objects work is that they maintain tables that point to various things: instance variables, class variables, and methods. The method table is usually called a virtual method table, or sometimes a vtable. Here’s what our Rational instance looks like:

instance variables Value
@numerator 3
@denominator 4
class pointer Rational

Our instance holds a reference to a class pointer Rational, and that class has a table of methods:

methods Code
isWhole? <#Method>
... ...

This isn’t exactly how it works, but it’s a good mental model. The implementation optimizes and reorganizes some things, but this is how most object oriented languages represent objects. Make sure you understand the distinction: classes hold methods, instances hold instance variables, and a pointer to the class.

Executing a method

So, let’s reimagine our cloud as structure like the above, and now let’s ask what happens. When we have an object rat of class Rational, we use the table to look up what code to run.

1 rat.isWhole?

So what happens?

  • We construct a message to send to rat, naming method isWhole?
  • We use rat’s vtable to look up and find the implementation of isWhole?, which is as follows:
1 def isWhole 
2   (@numerator % @denominator) == 0
3 end
  • We set self to point at the object rat
  • We start executing at the beginning of isWhole and go statement by statement.

(Note that this is a little bit of a lie, becuase of something called metaclasses, that we’ll talk about shortly.)

Classes are objects too

In Ruby, classes are objects too. Classes have methods, can receive messages, and the like. But it’s very important to remember that objects and classes are different.

If it’s helpful, think of it this way: classes are blueprints for objects. A class is like a factory that generates objects, and those objects are similar, but different. Think of it like this: Martin Guitars are made at a Martin factory. The factory has the basic blueprints, raw materials, and designs, for each of the guitars that come out of the factory. But when you physically receive a guitar, its yours. You can’t play a song on the factory, but you can play a song on the guitar. You can’t bash the factory against the stage, but you can bash the guitar.

It’s similar for objects. You can’t call .numerator on Rational, but you can call it on instances of Rational. You can’t call .add on Rational, but you can call it on instances of Rational. So what can you do with Rational? Well, Rational is a class, just like any other class. Let’s send a message to the Rational class to look up its methods:

 1 2.0.0-p0 :013 > ARational.methods
 2  => [:allocate, :new, :superclass, :freeze, :===, :==, :<=>, :<, :<=,
 3 :>, :>=, :to_s, :inspect, :included_modules, :include?, :name,
 4 :ancestors, :instance_methods, :public_instance_methods, 
 5 :protected_instance_methods, :private_instance_methods,
 6 :constants, :const_get, :const_set, :const_defined?,
 7 ...
 8  :send, :public_send, :respond_to?, :extend, 
 9 :display, :method, :public_method, :define_singleton_method, 
10 :object_id, :to_enum, :enum_for, :equal?, :!, :!=, 
11 :instance_eval, :instance_exec, :__send__, :__id__]

Wow! All of these thins we can do with a Rational. But perhaps the one we might use most: new.

Creating new objects

Let’s ask ourselves what the following code does:

1 ARational.new(3,4)

Well, new has special meaning as a method. When we call it, it creates a new object, does some things to make the proper connections and memory allocation, and then calls our initialize method in ARational, which defined like so:

1 def initialize(numerator,denominator=1)
2   @numerator = numerator
3   @denominator = denominator
4 end

Imagine that in the Ruby interpreter, we create a blank sheet that looks like this:

instance variables Value
... ...
class variables Value
... ...
superclass ...
methods Code
isWhole? <#Method>
... ...

Now, when we start executing initialize, we fill in the numerator and denominator variables to point at the proper things. Among other things, new sets the object’s method table to point at the methods from the defined class.

It’s all about self / Inheritance

When we start executing code, we need to know where self is. What do we do with self:

  • Look up methods
  • Define methods
  • Define instance variables
  • Send it to other objects as a parameter

When you call a method on an object in Ruby, it uses self to figure out how to resolve that method. So when we look at our previous rat instance, we call the method .isWhole? using the virtual method table associated with the ARational class. Why? Because, when we called new, it made rat’s method table point at ARational’s method definitions. Methods are looked up in the class in which they’re defined. But what happens when we do this:

1 a = ARational.new(3,4)
2 a.sum(ARational.new(1,2))

Well, first, ARational is created and set up according to the procedure we just talked about. Next, .sum’s parameters are evaluated, which sets up another ARational class. Next, we use ARational’s method table to try to find .sum. But we can’t! Because it’s not defined! So what do we do?

Superclasses

It turns out that each class has a superclass. When we can’t find methods, we take the superclass pointer and use it to climb up the inheritance chain, and for each ancestor (in order, from bottom to top of the chain), we try to resolve the method.

So, when we call a.sum, we look in ARational’s method table, and we can’t find .sum, so we go up to ANumber’s method table, and we do find it. We start executing code there.

Metaclasses

It turns out that, for every object you create in Ruby, there’s actually something a little bit sneaky going on. When you create an object a of class ARational, Ruby creates a secret subclass of ARational that is unique just to a. So, previously, I told you that A’s inheritance chain was:

ARational --> ANumber --> Object

But it’s really not quite that. It’s actually:

ARational<meta> --> ARational --> ANumber --> Object

So method resolution for a happens by searching through this secret class, then ARational, and etc… This secret class is called a metaclass in Ruby, and it’s not named explicitly. It’s unique to each object, so naming it would be strange. Note however, that if we create another object, b, it will have a different, and unique metaclass.

ARational<meta#a> --> ARational --> ANumber --> Object
                  --------^
ARational<meta#b> |

Adding methods to classes

We’ve talked about how instance variables are added to classes, but we haven’t talked about how methods are defined. Here’s the key distinction: instance variables are looked up on objects, and methods are looked up on classes. So, you would add a variable @a to a, but you would add a method .sum to ARational. This is perhaps confusing, so think hard, make sure you understand it, and ask questions if you don’t.

Now, let’s think about what Ruby does when we see this:

 1 class ANumber
 2   def to_s
 3     ""
 4   end
 5 
 6   def isWhole?
 7     false
 8   end
 9   
10   def sum(b)
11     self.add(b)
12   end
13 end

Well, when ruby sees the class keyword, it creates a blank class object, and allows you to add methods to that class. So when it sees .to_s, .isWhole?, and .sum, it adds those methods to the freshly created class. How does it know what classes to add methods to? Well, like everything else, Ruby uses self! If the interpreter is right at the point where we have the >> symbol:

1 class ANumber
2 >>  def to_s
3     ""
4   end
5 
6   # ...
7 end

What is self? It’s not an instance of ANumber, it’s literally the class object ANumber. When the interpreter sees the def keyword, it takes the ANumber class and puts a method in the method table.

Now, when we create a, an instance of ANumber, it’s class is ANumber, so it points at the methods defined there.

a -- class --> ARational<meta#a> -- super class --> ARational -- ... -->

So, when inside the class ARational, methods are defined on ARational, the class. And when inside a, (the object) methods are looked up on ARational. But actually, they’re looked up on ARational’s metaclass first.

(Note: metaclasses are also called eigenclasses, among a few other names)

So to define methods on a class, we need to have self point at that class.

Coding this up in Ruby

We won’t cover exactly how to code all of this up in Ruby today, but might touch on it later. The goal behind today’s lecture was to talk about how objects are implemented in object oriented languages, and how Ruby implements this. If you’re interested I highly recommend you read this article, or come talk to me during office hours.

There are four basic syntactic methods to change self:

mechanism method resolution method definition new scope?
class ARational ARational same yes
class << ARational ARational’s metaclass same yes
a.class_eval ARational same no
a.instance_eval ARational ARational’s metaclass no

Where a is an instance of the class ARational.

Acknowledgement

This article contains some great examples of metaprogramming that you can feel free to go through if you’d like to learn more. This book is the definitive reference on metaprogramming in Ruby. It’s very well written and a good read. I highly recommend it if you plan to ever do real Ruby programming. Understanding Ruby metaprogramming is central to being able to effectively understand all of the core Ruby concepts.