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!
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.
class User def initialize(name) @name = name end def name @name end end user = User.new('Thijs') user.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 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.
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.
class User # ... def self.all [new("Thijs"), new("Robert"), new("Tom")] end end User.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.
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.
thijs, robert, tom = User.all thijs.class # => User robert.class # => User tom.class # => User thijs.singleton_class # => #<Class:#<User:0x00007fb71a9a2cb0>> robert.singleton_class # => #<Class:#<User:0x00007fb71a9a2c60>> tom.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.
robert = User.new("Robert") def robert.last_name "Beekman" end robert.last_name # => "Beekman" User.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
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.
tom = User.new("Tom") def tom.last_name "de Bruijn" end
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.
class User # ... def self.all [new("Thijs"), new("Robert"), new("Tom")] end end
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 is an object stored in a constant, we’ll access the same object—and the same metaclass—whenever we reference it.
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.
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.
class User class << self self # => #<Class:User> def all [new("Thijs"), new("Robert"), new("Tom")] end end end User.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
User's metaclass instead of
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 (
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.
class User instance_eval do self # => User def all [new("Thijs"), new("Robert"), new("Tom")] end end end User.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.
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
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
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.