Object Orientation and Metaprogramming

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 endFor our purposes, let’s start out by assuming that objects are just clouds. I’ve drawn a Rational object’s cloud here:

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.

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 methodisWhole? - We use
rat’s vtable to look up and find the implementation ofisWhole?, which is as follows:
1 def isWhole
2 (@numerator % @denominator) == 0
3 end- We set
selfto point at the objectrat - We start executing at the beginning of
isWholeand 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 endImagine 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 endWell, 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 endWhat 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.