📜 ⬆️ ⬇️

How method scheduling in Ruby works

Translator's Notes:

Hello. This article is a free translation (link at the end). I do not pretend to any 100% correct translation. However, I believe that the general essence of what is happening is conveyed completely.

Who can this article be useful for? Most likely for beginners Ruby on Rails developers who are just curious to understand some points in the work of Ruby.
')
For whom this article may be useless? Most likely for purebred Ruby programmers and hardened Ruby on Rails developers. Chances are that you already know this.

Why did I do the translation? This article seemed to me interesting and I simply had a desire to share it with all the Russian-speaking (mb badly knowing English) community.

PS If you know English, just follow the link at the end.

The following is the translation text:

The other day, I asked everyone if anyone knew a good and short explanation regarding objects in Ruby and the method dispatching system. And the answer of some people was "no, you should write about it." So here is the article. I'm going to explain how the system of objects in Ruby works, including method searching, inheritance, super classes, classes, mixins, and singleton methods. My understanding came not from reading MRI sources, but from re-implementing this system, once in JavaScript and once in Ruby. If you want to read a small but almost correct implementation, then this is a good place to start.

Due to the fact that I did not really read the sources, this article will explain what is happening in Ruby from the point of view of logic, and not from the point of view of what is really happening. This is just a model with which you can understand some things.

Let's start over. You can create a system of cut objects almost completely only from modules. Think of modules as bags of methods. For example, module A contains the method foo and bar.

+----------+ | module A | +----------+ | def foo | | def bar | +----------+ 


When you write def foo ... end inside a ruby ​​module, you add this method to the module, that's all. A module can have multiple parents. When you write:

 module B include A end 


All you do is add And as a parent to B. No methods are copied, we simply create a pointer from B to A.

  +-----------+ | module A | +-----------+ | def foo | | def bar | +-----------+ ^ | +-----+-----+ | module B | +-----------+ | def hello | | def bye | +-----------+ 


A module can have multiple parents, thereby forming a tree. For example, these modules:

 module A def foo ; end def bar ; end end module B def hello ; end def bye ; end end module C include B def start ; end def stop ; end end module D include A include C end 


They form this tree, in accordance with the order of their inclusion.

  +-----------+ | module B | +-----------+ | def hello | | def bye | +-----------+ ^ +-----------+ +-----+-----+ | module A | | module C | +-----------+ +-----------+ | def foo | | def start | | def bar | | def stop | +-----------+ +-----------+ ^ ^ +-------------------+-------------------+ | +-----+-----+ | module D | +-----------+ 


An important point that explains how Ruby finds the place to define a method lies in the "genealogy" of models (module's 'ancestry'). You can ask the module to provide you with its “pedigree” and it will provide you with it in the form of an array of modules:

 >> D.ancestors => [D, C, B, A] 


The important thing is that it is a pedigree in the form of a simple chain, instead of being in the form of a tree. This chain determines the order in which we iterate over the modules to find a method. To build this list, we start with D and dive deeper, looking all the parents from right to left. Therefore, the order of include calls is very important. Parents of the module are arranged in order and this determines the order in which they will be searched.

When we want to find a place to define a method, we look at the chain of inheritance until we find the first module in which it is defined. If none of the modules contain this method, we search again, but this time we are looking for a method called method_missing. If none of the modules contain a method, then a NoMethodError exception is thrown. The chain of module inheritance solves the problem when two modules contain the same method. That method whose module is the first in the inheritance chain will be called.

We can use Ruby's capabilities to determine whose method was used when we called it.

 >> D.instance_method(:foo) => #<UnboundMethod: D(A)#foo> >> D.instance_method(:hello) => #<UnboundMethod: D(B)#hello> >> D.instance_method(:start) => #<UnboundMethod: D(C)#start> 


UnboundMethod is simply a representation of a method from a model, before it is associated with an object. When you see D (A) #foo, it means that D inherits the #foo method from A. If you call #foo on an object that includes D, you will get the method defined in A.

Speaking of objects, why have we not done one yet? What good is a bag with methods, if there is no object to which we could apply it. What this is where the class comes into action. In Ruby, a class is a subclass of a module, which sounds weird, but remember that they are all data structures that store methods. A class is almost like a module, from the point of view that it stores methods and may include other modules, but it has some additional features. One of which is the ability to create objects.

 class K include D end k = K.new 


We again have the opportunity to determine where the object's methods come from.

 >> k.method(:start) => #<Method: K(C)#start> 


This shows that when we call k.start, we get the #start method from module C. You notice that when you call the module instance_method, it returns the UnboundMethod, and in the case of the Method object. The difference is that Method is associated with an object. When you call #call on an object, the behavior will be the same as in the case of k.start. UnboundMethods cannot be called directly, because they do not have an object that could call them.

It looks like we are looking for a method starting from the class to which the object belongs, then we look through the entire chain of inheritance until we find the place to define the method. Well, it's almost true, but Ruby has another trick up his sleeve: singleton methods. You can add new methods to an object, and only to this object, without adding it to the class. See:

 >> def k.mart ; end >> k.method(:mart) => #<Method: #<K:0x00000001f78248>.mart> 


We can also add them to the modules, since modules are just one of the object types.

 >> def B.roll ; end >> B.method(:roll) => #<Method: B.roll> 


If the name of the method is (.) Instead of the hash (#), it means that the method exists only for this object, instead of being in a module. However, we said earlier that Ruby uses modules to store methods; simple old objects had no such possibility. So where are the singleton methods stored?

Each object in Ruby (and remember, modules and classes are also objects) have so-called metaclasses, also known as singleton classes, eigenclass or virtual classes. The work of these classes is to simply store the object's singleton methods. Initially, they do not contain any methods and have an object class as their only parent. So for our object k, the inheritance chain will look like this:

  +-----------+ | module B | +-----------+ ^ +-----------+ +-----+-----+ | module A | | module C | +-----------+ +-----------+ ^ ^ +-------------------+-------------------+ | +-----+-----+ | module D | +-----------+ ^ +-----+-----+ | class K | +-----------+ ^ +-----+-----+ +---+ | metaclass |<~~~~~~~~+ k | +-----------+ +---+ 


We can ask Ruby to show the object's metaclass. Here we see that the metaclass is an anonymous class attached to the object k, and it has an instance #mart method which does not exist in the K class.

 >> k.singleton_class => #<Class:#<K:0x00000001f78248>> >> k.singleton_class.instance_method(:mart) => #<UnboundMethod: #<Class:#<K:0x00000001f78248>>#mart> >> K.instance_method(:mart) NameError: undefined method `mart' for class `K' 


One point to which you should pay attention is that the metaclass is not displayed in the chain of inheritance, but you should understand that it still participates in the chain of searching for the place where the method is defined.

When we call the object method k, the object asks its metaclass if it does not contain this method and the metaclass further scans the chain of inheritance in order to determine the location of the method. Singleton methods are in metaclass and they have an advantage over methods defined in the class of the object and all its parents.

Now, we are approaching the second special class property, besides their ability to create objects. Classes have a special form of inheritance called “subclassing”. Each class has one and only one superclass, the default is Object. From the point of view of the method call, you can think of superclasses as the first parent module of the class:

 class Foo < Bar class Foo include Extras =~ include Bar end include Extras end 


Thus, the inheritance chain gives us [Foo, Extras, Bar], in both cases, and it, as it was before, determines the order of the search method. (In fact, it looks like [Foo, Extras, Bar, Object, Kernel, BasicObject], but we will look at it in a minute). Note that Ruby violates Liskov's principle of substitution, not allowing classes to be included (include), only modules can be used in a similar way, not their subtypes. The snippet shown above shows how subclassing affects the search order of a method, and the code on the right will not work if Bar is a class.

If subclassing is the same as inclusion (include), why do we need both of these features? Well, it gives us another opportunity. Classes inherit singleton methods from their superclasses, and this is not the case with modules.

 module Z def self.z ; :z ; end end class Bar def self.bar ; :bar ; end end class Foo < Bar include Z end # Singleton methods from Bar work on Foo ... >> Bar.bar => :bar >> Foo.bar => :bar # ... but singleton methods from Z don't >> Zz => :z >> Foo.z NoMethodError: undefined method `z' for Foo:Class 


We can model this in terms of parenting, saying that subclasses of metaclasses have superclass metaclasses as their parents.

  +-----+ +--------------+ | Bar +~~~~~~~~>| #<Class:Bar> | +-----+ +--------------+ ^ ^ | | +--+--+ +-------+------+ | Foo +~~~~~~~~>| #<Class:Foo> | +-----+ +--------------+ 


Indeed, if we look at Foo, we will see that its #bar method comes from the metaclass Bar.

 >> Foo.method(:bar) => #<Method: Foo(Bar).bar> >> Foo.singleton_class.instance_method(:bar) => #<UnboundMethod: #<Class:Bar>#bar> 


We have seen how inheritance and the order of searching for a method in Ruby can be depicted as a tree of modules, with inclusion and subclassing creating different parental relationships. We also explained the single and multiple inheritance methods of objects and singleton methods. Now let's look at a few things that are on the back of this model.

The first is the Object # extend method. By calling object.extend (M), we make the methods from module M available in the object. We do not copy methods, we simply add M as the parent of the metaclass of this object. If the object has a class of Thing, we get the following relation:

  +-------+ +-----+ | Thing | | M | +-------+ +-----+ ^ ^ +-------+-----+ | +--------+ +---------+-------+ | object +~~~~~~~~>| #<Class:object> | +--------+ +-----------------+ 


Thus, the extension of an object using a module is the same as the inclusion of this module in the metaclass of an object. (Actually, there is some difference, but this does not apply to this topic). Looking at this tree, we see that when we call the object method, the method dispatching system will prefer the methods defined in module M to those in Thing (the method from M will be used), and, in turn, the methods found in object metaclass will be prioritized than M and Thing.

This context is important: we cannot say that methods in M ​​take precedence over Thing in a general sense, but only when we are talking about calling the method of an object. The chain of inheritance where the method is looked for is what matters. And this shows up when we examine the work of super. Take a look at the following set of modules:

 module X def call ; [:x] ; end end module Y def call ; super + [:y] ; end end class Test include X include Y end 


The inheritance chain for Test is as follows: [Test, Y, X], so if we call Test.new.call, we call the #call method from Y. But, what happens when Y calls super? Y does not have its own chain of inheritance, that is, there is no one on whom Y could call this method, right?

And no. When we were faced with a super call, what is important is that we made a method call for the object's inheritance chain, that's all. You can imagine the search for a method as the search for all the definitions of a given method in the inheritance chains of an object's metaclass.

 >> t = Test.new >> t.singleton_class.ancestors.map { |m| m.instance_methods(false).include?(:call) ? m.instance_method(:call) : nil }.compact => [#<UnboundMethod: Y#call>, #<UnboundMethod: X#call>] 


To determine the location of the method, we call the first method in the inheritance chain. If this method calls super, we jump to the next one, and so on, until we finish the search. If Test did not include the module X, there would be no implementation of #call other than that defined in Y, so the call to super would lead to an error.

In fact, in our case, Test.new.call will return [: x,: y].

We are almost done, but I promised to tell you what Object, Kernel, and BasicObject are. BasicObject is the root class of the entire system; This is a class without a superclass. Object is inherited from BasicObject, and is the base superclass for all user classes. The difference between them is that BasicObject has almost no methods, while Object has quite a lot: Ruby kernel methods, such as: # ==, #__send__, #dup, #inspect, #instance_eval, #is_a ?, # method, #respond_to ?, and #to_s. Although, in fact, Object itself does not contain all these methods, it gets them from Kernel. Kernel is just a module with a set of all the methods of an object from the ruby ​​kernel. Therefore, if we try to display the Ruby kernel object system, we get the following:

  +---------------+ +------------+ | | | | | +-----------+----------+ +-------------+ +--------+ +--------+--------+ | | | #<Class:BasicObject> |<~~~~+ BasicObject | | Kernel +~~~~>| #<Class:Kernel> | | | +----------------------+ +-------------+ +--------+ +-----------------+ | | ^ ^ ^ | | | +-------+--------+ | | | | | | +--------+--------+ +----+---+ | | | #<Class:Object> |<~~~~~~~~~~~~~~~~+ Object | | | +-----------------+ +--------+ | | ^ ^ | | | | | | +--------+--------+ +----+---+ | | | #<Class:Module> |<~~~~~~~~~~~~~~~~+ Module |<-----------------------------------+ | +-----------------+ +--------+ | ^ ^ | | | | +--------+--------+ +----+---+ | | #<Class:Class> |<~~~~~~~~~~~~~~~~+ Class | | +-----------------+ +--------+ | ^ | | +-----------------------------------------------+ 


This diagram shows the modules and classes of the Ruby kernel: BasicObject, Kernel, Object, Module, and Class, their metaclasses, and how they are all related. Yes, BasicObject.singleton_class.superclass is Class. Ruby makes a little voodoo magic to make this barrel organ (note of the translator). In any case, if you want to understand the dispatching methods in Ruby, just remember:



No, I do not know all the subtleties of this work. Nobody knows.

Original article: blog.jcoglan.com/2013/05/08/how-ruby-method-dispatch-works

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


All Articles