How the Ruby Interpreter Creates Methods on the Fly

Posted by on February 28, 2024

(And why it matters!)

I was lucky enough to attend last year’s EuRuKo, the travelling European Ruby conference. A theme of the conference (for me) was Ruby’s infamous embrace of metaprogramming, which I’ve had little exposure to in my day-to-day as a Rails dev.

The approach to this discussion was inspired by this great talk by Masafumi Okura on Code Reading, and much of the detail comes from the book Ruby under a Microscope by Pat Shaughnessy, which was recommended to me over coffee between talks.

The thing I want to discuss today is the concept of Singleton classes. 

We have to start off with a disclaimer, which is that the term “singleton” refers to multiple concepts. In principle, it just means “there’s only one of these”. The most popular concept referred to as a singleton class is when a class only allows one instance of itself to exist; Ruby even features a Singleton module to implement this.

The concept we’re looking at today is entirely different, but Ruby also (confusingly) refers to it as a singleton class. This singleton class exists to hold methods defined at runtime.

Let’s start with this thought experiment:

obj = Object.new

def obj.my_new_method
  "where do I live, though?"
end

In the above example, we create a new object from the super-est of all super-classes, Object. Then, using the confusing syntax def instance_name.method_name we define a new method on our object. This new method can be called from our object as though it had been defined on the object’s class:

obj.my_new_method
=> "where do I live, though?"

But, I hear you cry, my_new_method can’t be defined on the object’s class! The object’s class is Object, and if we had defined a new method on Object, that super-est of super-classes, every object in my program would be able to invoke my_new_method!

And indeed, we see that other instances of object do not have access to this new method:

obj2 = Object.new
obj2.my_new_method
undefined method `my_new_method' for #<Object:0x00000001054b7458> (NoMethodError)

Tellingly, we can even grep Object’s instance method list. This searches all the methods that Object gives its instances access to:

Object.instance_methods.grep(/my_new_method/)
=> []

And yet, if we grep our original object’s method list, we find my_new_method:

obj.methods.grep(/my_new_method/)
=> [:my_new_method]

Notice the difference between methods and instance_methods – instance_methods is something you call on a class to determine what methods its instances will inherit, whereas methods is something you call on an object to determine what methods it will respond to.

Part I: Formulating a Hypothesis

my_new_method has to be defined somewhere, but it clearly isn’t on Object. Let’s try and figure out where!

Imagine you are writing your own programming language, and you want it to be able to add new methods to specific objects at runtime. But, you don’t want to add the new method to other instances of the same class.

You open up the source code for your method lookup algorithm. At the minute, it’s very clean and elegant: the object checks if the method exists on its class, and then on its super-class, and then on its super-super-class, all the way up to Object. If it doesn’t find the method at any level of the class hierarchy, it throws the no_method_error we saw a moment ago.

You probably don’t want to start adding special cases to this algorithm. But there is a way to achieve what you want without changing it at all: you can add a secret class to your hierarchy!

When the user adds the runtime method, create a brand new class to hold the method. Let’s call it SecretClass. Because you’re the interpreter and you can do what you like, you can hotswap the class of obj from Object to SecretClass at the moment the method is defined. Then to make sure obj still inherits from Object as the user specified, we can make Object the super-class of SecretClass.

Our existing method lookup algorithm works like a charm! It finds my_new_method on SecretClass without a problem, but other instances of Object are none the wiser.

Part II: Does it actually work like this, though?

Let’s test our hypothesis.

The most obvious test is simply to ask obj what its class is. If, as we speculate, Ruby has inserted a SecretClass, we would expect it to no longer be Object:

obj.class
=> Object

Huh! The class of obj is, in fact, Object. Are we back to the drawing board then? The answer is no, we aren’t. Because Ruby is lying to us!

The secret to working this out is the singleton_class method:

obj.singleton_class
=> #<Class:#<Object:0x0000000104aa5190>>

This is some opaque console output, but we can make an educated guess: we have the words Class and Object and something that looks like a memory address, so we might think that this is a reference to a class object. A class object that Ruby doesn’t want us to know about!

The last step to confirm our theory is to run the same experiments we ran on Object earlier. We expect obj.singleton_class to hold our runtime-defined method. And indeed:

obj.singleton_class.instance_methods.grep(/my_new_method/)
=> [:my_new_method]

So we can be confident that it does work this way! This singleton_class object is the SecretClass we defined in our hypothetical earlier.

Part III: That’s cool, but…

All this metaprogramming stuff isn’t something you’re ever going to realistically use, right? It’s very interesting, and Ruby is correct and principled to implement it, but when rubber hits the road, you’re not going to find anything like this in your codebase, are you?

And indeed, if you’ve heard this explanation of singleton classes before, it probably stopped around here, for pretty much this reason. Nobody actually needs to define methods at runtime, right?

Wrong!

Let’s take a look at this code, which you probably find pretty familiar:

class MyClass
  def self.new_class_method
    "Nothing to see here, I'm a class method!"
  end
end

Let’s do a little refactoring on this code. Firstly, let’s consider that self keyword; it’s just an alias for the class name MyClass, when nested in the class MyClass block. If we use the actual name, we can write the same code like this:

class MyClass; end

def MyClass.new_class_method
  "...where do class methods live, though?"
end

And now, let’s return to our very first code snippet from earlier, where we define a new method on a generic object:

obj = Object.new

def obj.my_new_method
  "where do I live, though?"
end

The final step is to remember that in Ruby, Everything is an Object. MyClass is an object just as obj is an object – and all objects have a class. In the case of MyClass, that class is the Class class, and we can see this clearly if we do one final refactor:

MyClass = Class.new
def MyClass.new_class_method
  "...where do class methods live, though?"
end

These aren’t just superficial similarities! We can’t have defined new_class_method on the Class class, or all classes would have access to new_class_method. We’ve defined a method on the singleton class of MyClass, exactly analogous to our first example.

But by now, you don’t need to take my word for it. Let’s check the same way as before:

Class.instance_methods.grep(/my_class_method/)
=> []
MyClass.singleton_class.instance_methods.grep(/my_class_method/)
=> [:my_class_method]

And there we have it! We see that class methods are implemented using the singleton class pattern – so next time you define a class method, remember that metaprogramming does have practical uses!

Leave a reply

Your email address will not be published. Required fields are marked *