📜 ⬆️ ⬇️

Understand Ruby Metaclasses


Translator's note: this post is a logical development, or rather the “prehistory” of the post. We delve into the include and extend and was prompted in the comments to it by the user murr , for which he thanks a lot.

Classes and objects in Ruby are intertwined quite intricately, and it’s not immediately obvious what’s what. Because of the special purpose of classes, it is very easy to lose sight of the fact that classes are also Ruby objects. Two things distinguish them from “ordinary” objects: they serve as a model for creating new objects and they are part of the class hierarchy. That's right, classes can have instances of themselves (objects), superclasses (parents), and subclasses (children).

If classes are objects, then they must have their own class. The class of all classes (as objects) in Ruby is the class Class :
')
#       Dog = Class.new #     class Dog # -    end Dog.class => Class 


Method Search Path


The relationship between classes and objects becomes important in the context of a method search path. When you call a method for an object, Ruby first looks for this method in the object's class. If the method cannot be found there, the next one will be checked by the superclass of the object class and so on across the entire class hierarchy to the very first class, which in Ruby 1.9 is BasicObject . The diagram below illustrates how Ruby traverses the class hierarchy in search of the right method:



The method search path for classes also works. When you call a class method, you first look at the class of the object, which we found out to be the Class class, then the search will continue in the Module superclass and so on until BasicObject .

 Dog.class => Class Class.superclass => Module Module.superclass => Object Object.superclass => BasicObject 

And the same visual:



As we can see, BasicObject and Object are the root of the class hierarchy, which means that all objects, no matter whether they are ordinary or this classes (as objects), inherit the methods of class instances defined in these classes.

Singleton methods


The main function of classes is to define the behavior of their instances. For example, the general behavior of dogs is located in the class Dog . However, in Ruby we can add unique behavior to individual objects. Those. we can add methods to a separate object of the Dog class that will not be available to other instances of this class. Most often, such methods are called singleton methods, because they belong to only one single object.

Once again, singleton methods are defined directly for an object, and not within the class of this object.

 snoopy = Dog.new def snoopy.alter_ego "Red Baron" end snoopy.alter_ego # => "Red Baron" fido.alter_ego # => NoMethodError: undefined method `alter_ego' for #<Dog:0x0000000190cee0> 


Metaclasses


As noted above, when we call a method for an object, Ruby looks for it in an object class. In the case of singleton methods, it is obvious that they are outside the class of the object, because they are not available to other instances of this class. So where are they? And how does Ruby manage to find singleton methods on a par with ordinary class instance methods without disrupting the normal method search path?

It turns out that Ruby implements this in his characteristic graceful style. First, an anonymous class is created to place the object's singleton methods in it. Then this anonymous class takes the role of an object class, and the object class becomes the superclass of an anonymous class. Thus, the usual pattern of the method search remains unchanged - both the singleton methods (in the anonymous class) and the methods of the class instance (in the superclass of the anonymous class) will be found by following the usual method search path (see the diagram below).

This dynamically created anonymous class has many names: metaclass, halo class (ghost), singleton class, and eigenclass. The word "Eigen" came from German and translates roughly as "your own."

Translator’s note: the author uses the term “eigenclass” later in the text (as in the original title), however, I chose the term “metaclass” for translation, because He is the least jarred on the ear of the Russian ear, in my opinion of course. The meaning of this does not change.



To the note: in addition to the definition of singleton methods, using the name of the object (ie, def snoopy.alter_ego ), you can also use a special syntax to access the object's metaclass:

 class << snoopy def alter_ego "Red Baron" end end 

The “ class << ” construct opens the metaclass of any object that you specify, and gives you the opportunity to work directly within the scope of this metaclass.

Class methods


As objects, classes can also have singleton methods. In truth, what we are accustomed to consider as class methods is in fact nothing more than singlton methods — methods that belong to a single object that, as it happens, is a class. Like any singleton methods, class methods are located inside the metaclass.

Class methods can be defined in various ways:

 class Dog def self.closest_relative "wolf" end end class Dog class << self def closest_relative "wolf" end end end def Dog.closest_relative "wolf" end class << Dog def closest_relative "wolf" end end 

All the examples above are identical: they all open the metaclass of the Dog object and define the method (class) in it.

Exposing metaclasses


Metaclasses are not only anonymous, under normal circumstances they are hidden from us. And this is despite the fact that they are designed to be the first stop on the path to finding a method:

 class << Dog def closest_relative "wolf" end end Dog.class # => Class 

In the code above, the Dog class continues to tell us that its class is a Class , although we added a class method to it, which should have resulted in the creation of a metaclass and the replacement of the class of the Dog object with its metaclass. However, there is a way to help us manifest the object's metaclass:

 class Object def metaclass class << self self end end end 

This code can be confusing at first, so let's look at it in more detail.

First of all, we know that the Object class is the ancestor of absolutely all classes in Ruby, so this is a good place to define the methods we want to make available everywhere and everywhere.

To understand the code above, we need to track where and how the value of self changes. Directly inside the metaclass () method, self is the object for which we called this method. Next, we open the metaclass of this object using the “ class << ” syntax. Now that we are inside the metaclass scope, the value of self changes and now refers to the metaclass of the object. Returning self from the metaclass, we call the to_s object identifier embedded in Ruby, which allows us to see a glimpse of the elusive metaclass.

Here's how we can use the metaclass () method to catch the metaclass.

 class Dog end snoopy = Dog.new => #<Dog:0x00000001c4a170> snoopy.metaclass => #<Class:#<Dog:0x00000001c4a170>> snoopy.metaclass.superclass => Dog 


Ruby's “to_s” method


The to_s method is defined in the Object class and returns a string representation of the object for which it was called. The string is a composition of the class name of the object and the unique identifier of the object. For example, # <Mouse: 0x00000001c4a170> .

Classes (instances of class Class ), such as Class , Object, or String , override this method so that it simply returns their name.

 Dog.to_s => "Dog" 

In fact, the to_s method is overridden in the Module class, which is the superclass of the Class class. Here’s how the to_s method is described in the Ruby documentation for the Module class:

Returns a string representing the module or class. For base classes and modules, this is their name. For singletons, we also show information about the object to which they are attached.

Pay attention to the remark about singletons. This explains why when we call to_s for the metaclass snoopy, we get an unnamed "Class" followed by the identifier of the object to which this class is attached: # <Class: # <Dog: 0x00000001c4a170 >> . By invoking to_s for the metaclass superclass, which refers to the original (not metaclass) class Dog , we just get his name: "Dog".

Metaclasses and class inheritance


Just as objects have access to the class instance methods defined in the superclasses of their class, also classes have access to the class methods defined in their ancestors.

 class Mammal def self.warm_blooded? true end end class Dog < Mammal def self.closest_relative "wolf" end end Dog.closest_relative # => "wolf" Dog.warm_blooded? # => true 

Another problem. How do classes manage to inherit the class methods of their ancestors? After all, the superclass of the class is on the path of finding the method of the class instances, but it is not on the path of finding the method for the class itself.

Method search path for an instance of the Dog class:
 fido -> Dog -> Mammal -> Object... 

Method Search Path for Dog Object:
 Dog -> Class -> Module -> Object... 

Here again, an interesting maneuver is used to ensure the inviolability of the method search pattern, and at the same time provide access to the methods of the superclass class.

First of all, when we define a class method (singleton method) for a Dog class, a metaclass is created. This metaclass then becomes a Dog object class instead of a Class class. The Class class is “pushed out” higher up the search chain of methods and becomes the superclass of the metaclass. Now, when we define a class method for the Dog class superclass (for example, for Mammal ), the created metaclass will become the superclass of the Dog object metaclass, “pushing” the Class class even higher. Since this explanation is as transparent as oil, you will see a diagram that sheds light on the above.



Here, get it. The path to finding a method for an object in Ruby in all its glory. Almost. We have not considered where modulus impurities fit in here. I hope that I will have time to show you this in another post.



Translator's note: here is where the impurities fit in and is explained in my translation: Understand the include and extend , so I recommend reading it for completeness (if you haven’t read it of course yet).

Source: https://habr.com/ru/post/143990/


All Articles