Revisiting the self and singleton class in Ruby

January 28, 2024

In Ruby, everything is an object, and every object has an anonymous class, which defines the methods the object can respond to. This anonymous class is called the singleton class.

ruby-singleton-class-inheritance

When calling a method on an object, Ruby will perform the method lookup by first checking on the object’s singleton class, before traversing the rest of the method chain.

Ruby has no class methods

The class methods are just instance methods on its singleton class.

class Animal
  def self.all; end
end

Animal.singleton_methods
#=> [:all]
Animal.singleton_class.instance_method(:all)
#=> #<UnboundMethod: #<Class:Animal>#all()>

The "current class"

Ruby always holds a reference to the current class, which is called "default definee" by Yugui of the Ruby core team. Thus, if you define a method without giving an explicit receiver, the current class will have the method as an instance method.

class Animal
  def weight; end
end

Animal.instance_method(:weight)
#=> #<UnboundMethod: Animal#weight()>

If you give a receiver to a method definition, the method will be added into the singleton class of the receiver.

word = "hello"
def word.spell; end

word.singleton_class.instance_method(:spell)
#=> #<UnboundMethod: #<Class:#<String:0x00007fcda4958890>>#spell()>

The class syntax changes both self and the current class to the class which is being defined. However, method definition doesn't.

class Foobar
  def foo
    def bar; end
    def self.baz; end
  end
end

f = Foobar.new
f.foo

Foobar.instance_method(:foo)
#=> #<UnboundMethod: Foobar#foo()>
Foobar.instance_method(:bar)
#=> #<UnboundMethod: Foobar#bar()>
Foobar.singleton_methods
#=> []
f.singleton_methods
#=> [:baz]

The eval methods

In Ruby, instance_eval and class_eval provide that provide the ability to modify a class or an object. The names are very similar, and their behavior is counterintuitive.

  • Use ClassName#instance_eval to define a class method (one associated with the class object but not visible to instances).
  • Use ClassName#class_eval to define an instance method (one that applies to all of the instances of ClassName).

To understand why this is true, let’s see what happens when we call the eval methods.

The instance_eval changes self to the receiver, the current class to its singleton class.

class Animal
  def weight; end
end

Animal.instance_eval do
  def all; end
end

Animal.instance_method(:all)
#=> NameError (undefined method `all' for class `Animal')

Animal.singleton_class.instance_method(:all)
#=> #<UnboundMethod: #<Class:Animal>#all()>

The class_eval changes both self and the current class to the receiver.

class Animal; end

Animal.class_eval do
  def weight; 1 end
end

Animal.instance_method(:weight)
#=> #<UnboundMethod: Animal#weight()>

Animal.new.weight
#=> 1

Animal.weight
#=> NoMethodError (undefined method `weight' for Animal:Class)

Open classes

Ruby supports a concept known as "Open classes", which opens the object's singleton class. This is equivalent to giving a receiver a method definition.

class Example
  class << self
    def foo; end
  end

  def self.bar; end
end

class << Example
  def baz; end
end

Example.singleton_methods
#=> [:foo, :bar, :baz]

Let's go through some examples:

class Foobar; end

Foobar.instance_eval do
  self #=> Foobar
  def method_by_instance_eval; end
end

Foobar.class_eval do
  self #=> Foobar
  def method_by_class_eval; end
end

class << Foobar
  self #=> #<Class:Foobar>
  def method_by_open_class; end
end

Foobar.instance_methods
#=> [:method_by_class_eval, ...]

Foobar.singleton_methods
#=> [:method_by_instance_eval, :method_by_open_class]

The above context changes can be summarized in the following table:

self current class
class_eval the receiver the receiver
instance_eval the receiver singleton class of the receiver
class << receiver singleton class of the receiver singleton class of the receiver

Takeaways

In Ruby,

  1. everything is an object, and every object has a singleton class;
  2. there are no class methods;
  3. the instance_eval changes self to the receiver, the current class to its singleton class;
  4. the class_eval changes both self and the current class to the receiver;
  5. what "open classes" does is opening the singleton class of the receiver object.

References


Site logo

This is an experimental blog that we are still working on.