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.
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
. 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.
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.
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.
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 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.
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
'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.
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 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.
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.
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.