I would like to provide a more concrete definition which clarifies the lookup algorithm.
First, let's define self
. self in Ruby is a special variable that always references the current object. The current object (self) is the default receiver on method calls. Second, self is where instance variables are found.
class MyClass
def method_one
@var = 'var'
method_two
end
def method_two
puts "@var is #{@var}"
end
end
obj = MyClass.new
obj.method_one
Above, when we call method_one, self will refer to the object instantiated, since we invoked method_one on an explicit receiver (the object instance). so self.method_one in the method definition in the Class will refer to the object instance not the Class object itself. @var will be stored in self. Note that when method_two is called, since there is no default receiver, the receiver is self. So when method_two is called, we remain in the same object instance. That is why @var in method_two will refer to the same @var in method_one. It is the same object.
Ruby supports inheritance. So if we call a method on self and it is not defined in its class, then Ruby will search for the instance method in the superclass. This happens until Ruby gets to BasicObject. If it cannot find the method in any of the superclasses, it raises a NoMethodError.
Now there is another important piece to the inheritance chain. There is an anonymous class called the singleton class that Ruby will inject into this inheritance chain. Where in the inheritance chain? It inserts it right before the original class of the object. This way, when ruby searches for the method, it will hit the singleton class before it hits the original class of the object.
> msg = 'Hello World'
=> "Hello World"
> def msg.hello_downcase
> puts 'Hello World'.downcase
> end
=> :hello_downcase
> msg.downcase
=> "hello world"
> msg2 = 'Goodbye'
=> "Goodbye"
> msg2.hellow_downcase
NoMethodError: undefined method `hellow_downcase' for "Goodbye":String
The lookup algorithm:
msg -> Anonymous Singleton Class (hello_downcase method is in here) -> String -> Object
msg2 -> String -> Object
In the above example, msg and msg2 are both object instances of the String class object. But we only opened up the singleton class of msg and not msg2. the hello_downcase
method was inserted into the singleton class of msg. It's important to note that when we add another singleton method, it will reopen the same singleton class again; it will not open another anonymous singleton class. There will only be one anonymous singleton class per instance.
Notice above I said the String class object, and not just the String class. That's because a class itself is an object. A class name is simply a constant which points to an object:
class HelloWorld
def say_hi
puts 'Hello World'
end
end
More precisely, in the above example, HelloWorld is a constant which points to an object, whose class is Class. Because of this, the lookup chain will be different for HelloWorld and its instances. An instance's class is HelloWorld. And when we invoke a method with the instance as the receiver, inside HelloWorld method definitions, self will refer to that instance. Now HelloWorld's class is Class (Since Class.new is what created HelloWorld). And because of this, its inheritance chain looks different:
#<HelloWorld:0x007fa37103df38> -> HelloWorld -> Object
HelloWorld -> Class -> Module -> Object
Now since HelloWorld is also an object, just as with instances, we can open up its Singleton Class.
class HelloWorld
def self.say_hi_from_singleton_class
puts 'Hello World from the Singleton Class'
end
end
HelloWorld.say_hi_from_singleton_class
The lookup algorithm:
HelloWorld -> Anonymous Singleton Class -> Class (this is where the new method is defined) -> Module -> Object
Why does this work? As mentioned, a method call with an explicit receiver changes the value of self to point to that object. The second thing that changes the value of self is a class definition. self inside of the class definition refers to the class object, referred to by constant HelloWorld. This is only the case while inside of the class definition. Once we leave the class definition, self will no longer refer to the constant HelloWorld.
> puts self
main
> class HelloWorld
> puts self
> end
HelloWorld
=> nil
> puts self
main
Ultimately, there are two ways in which the special variable self will change: 1) when you call a method with an explicit receiver, 2) inside of a class definition.