ruby

Unraveling Classes, Instances and Metaclasses in Ruby

Jeff Kreeftmeijer

Jeff Kreeftmeijer on

Unraveling Classes, Instances and Metaclasses in Ruby

Welcome to a new episode of Ruby Magic! This month's edition is all about metaclasses, a subject sparked by a discussion between two developers (Hi Maud!).

Through examining metaclasses, we'll learn how class and instance methods work in Ruby. Along the way, discover the difference between defining a method by passing an explicit "definee" and using class << self or instance_eval. Let's go!

Class Instances and Instance Methods

To understand why metaclasses are used in Ruby, we'll start by examining what the differences are between instance- and class methods.

In Ruby, a class is an object that defines a blueprint to create other objects. Classes define which methods are available on any instance of that class.

Defining a method inside a class creates an instance method on that class. Any future instance of that class will have that method available.

1class User
2  def initialize(name)
3    @name = name
4  end
5
6  def name
7    @name
8  end
9end
10
11user = User.new('Thijs')
12user.name # => "Thijs"

In this example, we create a class named User, with an instance method named #name that returns the user's name. Using the class, we then create a class instance and store it in a variable named user. Since user is an instance of the User class, it has the #name method available.

A class stores its instance methods in its method table. Any instance of that class refers to its class’ method table to get access to its instance methods.

Class Objects

A class method is a method that can be called directly on the class without having to create an instance first. A class method is created by prefixing its name with self. when defining it.

A class is itself an object. A constant refers to the class object, so class methods defined on it can be called from anywhere in the application.

1class User
2  # ...
3
4  def self.all
5    [new("Thijs"), new("Robert"), new("Tom")]
6  end
7end
8
9User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]

Methods defined with a self.-prefix aren’t added to the class’s method table. They’re instead added to the class’ metaclass.

Metaclasses

Aside from a class, each object in Ruby has a hidden metaclass. Metaclasses are singletons, meaning they belong to a single object. If you create multiple instances of a class, they’ll share the same class, but they’ll all have separate metaclasses.

1thijs, robert, tom = User.all
2
3thijs.class # => User
4robert.class # => User
5tom.class # => User
6
7thijs.singleton_class  # => #<Class:#<User:0x00007fb71a9a2cb0>>
8robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>>
9tom.singleton_class    # => #<Class:#<User:0x00007fb71a9a2c10>>

In this example, we see that although each of the objects has the class User, their singleton classes have different object IDs, meaning they’re separate objects.

By having access to a metaclass, Ruby allows adding methods directly to existing objects. Doing so won’t add a new method to the object’s class.

1robert = User.new("Robert")
2
3def robert.last_name
4  "Beekman"
5end
6
7robert.last_name # => "Beekman"
8User.new("Tom").last_name # => NoMethodError (undefined method `last_name' for #<User:0x00007fe1cb116408>)

In this example, we add a #last_name to the user stored in the robert variable. Although robert is an instance of User, any newly created instances of User won’t have access to the #last_name method, as it only exists on robert’s metaclass.

What Is self?

When defining a method and passing a receiver, the new method is added to the receiver’s metaclass, instead of adding it to the class’ method table.

1tom = User.new("Tom")
2
3def tom.last_name
4  "de Bruijn"
5end

In the example above, we've added #last_name directly on the tom object, by passing tom as the receiver when defining the method.

This is also how it works for class methods.

1class User
2  # ...
3
4  def self.all
5    [new("Thijs"), new("Robert"), new("Tom")]
6  end
7end

Here, we explicitly pass self as a receiver when creating the .all method. In a class definition, self refers to the class (User in this case), so the .all method gets added to User's metaclass.

Because User is an object stored in a constant, we’ll access the same object—and the same metaclass—whenever we reference it.

Opening the Metaclass

We’ve learned that class methods are methods in the class object’s metaclass. Knowing this, we’ll look at some other techniques of creating class methods that you might have seen before.

class << self

Although it has gone out of style a bit, some libraries use class << self to define class methods. This syntax trick opens up the current class's metaclass and interacts with it directly.

1class User
2  class << self
3    self # => #<Class:User>
4
5    def all
6      [new("Thijs"), new("Robert"), new("Tom")]
7    end
8  end
9end
10
11User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]

This example creates a class method named User.all by adding a method to User's metaclass. Instead of explicitly passing a receiver for the method as we saw previously, we set self to User's metaclass instead of User itself.

As we learned before, any method definition without an explicit receiver gets added as an instance method of the current class. Inside the block, the current class is User's metaclass (#<Class:User>).

instance_eval

Another option is by using instance_eval, which does the same thing with one major difference. Although the class's metaclass receives the methods defined in the block, self remains a reference to the main class.

1class User
2  instance_eval do
3    self # => User
4
5    def all
6      [new("Thijs"), new("Robert"), new("Tom")]
7    end
8  end
9end
10
11User.all # => [#<User:0x00007fb01701efb8 @name="Thijs">, #<User:0x00007fb01701ef68 @name="Robert">, #<User:0x00007fb01701ef18 @name="Tom">]

In this example, we define an instance method on User's metaclass just like before, but self still points to User. Although it usually points to the same object, the "default definee" and self can point to different objects.

What We've Learned

We've learned that classes are the only objects that can have methods, and that instance methods are actually methods on an object's metaclass. We know that class << self simply swaps self around to allow you to define methods on the metaclass, and we know that instance_eval does mostly the same thing (but without touching self).

Although you won't explicitly work with metaclasses, Ruby uses them extensively under the hood. Knowing what happens when you define a method can help you understand why Ruby behaves like it does (and why you have to prefix class methods with self.).

Thanks for reading. If you liked what you read, you might like to subscribe to Ruby Magic to receive an e-mail when we publish a new article about once a month.

Share this article

RSS

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps