[Из песочницы] Как работает диспетчеризация методов в Ruby

Примечания переводчика:

Всем привет. Данная статья является вольным переводом (ссылка в конце). Я не претендую на какую либо 100% корректность перевода. Однако считаю, что общую суть происходящего передал полностью.

Для кого может быть полезна эта статья? Скорее всего, для начинающих Ruby on Rails разработчиков, кому просто интересно понять некоторые моменты в работе Ruby.

Для кого эта статья может быть бесполезна? Скорее всего, для чистокровных Ruby программистов и прожженых Ruby on Rails разработчиков. Высока вероятность того, что вы это уже знаете.

Зачем я сделал перевод? Эта статья мне показалась интересной и внутри меня просто появилось желание поделиться ею со всем русскоговорящим (м.б. плохо знающим английским) сообществом.

P.S. Если знаете английский, просто перейдите по ссылке в конце.
Далее следует текст перевода:

На днях, я спрашивал у всех, знает ли кто-нибудь хорошее и короткое объяснение относительно объектов в Ruby и системы диспетчеризации методов. И ответ некоторых людей был «нет, тебе следует написать об этом». Так что вот статья. Я собираюсь объяснить как работает система объектов в руби, включая поиск метода, наследование, супер классы, классы, примеси (mixins) и singleton методы. Мое понимание пришло не из прочтения MRI источников, но из повторной реализации этой системы, один раз в JavaScript и один раз в руби. Если вы хотите прочесть малую, но почти правильную реализацию, тогда эта статься неплохое место, чтобы начать.

По причине того, что я в действительности не читал источники, эта статься объяснит происходящее в руби с точки зрения логики, а не с точки зрения что реально происходит. Это просто модель, с помощью которой вы сможете понять некоторые вещи.

Давайте начнем сначала. Вы можете создать систему объектов руби почти полностью только из модулей. Думайте о модулях как о мешках с методами. Для примера, модуль А содержит метод foo и bar.

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

Когда вы пишете def foo… end внутри руби модуля, вы добавляете этот метод в модуль, вот и все. Модуль может иметь нескольких родителей. Когда вы пишите:

module B
  include A
end

Все что вы делаете — это добавляете А как родителя к В. Никакие методы не копируются, мы просто создаем указатель с В на А.

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

Модуль может иметь нескольких родителей, тем самым формируя дерево. К примеру вот эти модули:

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

Они формируют данное дерево, в соответствии с порядком их включения.

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

Важный момент который объяснит как руби отыскивает место определения метода заключается в «родословной» моделей (module«s 'ancestry»). Вы можете попросить модуль предоставить вам его «родословную» и он предоставит вам её в виде массива модулей:

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

Важная вещь заключается в том, что это родословная в виде простой цепочки, вместо того чтобы быть в виде дерева. Эта цепочка определяет порядок, в котором мы перебираем модули, чтобы найти метод. Чтобы построить этот список, мы начинаем с D и погружаемся глубже, просматривая всех родителей справа налево. Поэтому порядок include вызовов очень важен. Родители модуля выстроены по порядку и это определяет порядок, по которому в них будет осуществляться поиск.

Когда мы хотим найти место определения метода, мы просматриваем цепочку наследования до тех пор, пока не найдем первый модуль, в котором он определен. Если ни один из модулей не содержит этот метод, мы выполняем поиск снова, но на этот раз мы ищем метод, который называется method_missing. Если ни один из модулей не содержит метод, то выдается исключение NoMethodError. Цепочка наследования модулей решает проблему, когда два модуля содержат один и тот же метод. Будет вызван тот метод, чей модуль оказался первым в цепочке наследования.

Мы можем использовать возможности руби, чтобы определить, чей метод был использован, когда мы его вызвали.

>> D.instance_method(:foo)
=> #

>> D.instance_method(:hello)
=> #

>> D.instance_method(:start)
=> #

UnboundMethod — это просто представление метода из модели, до того как он будет связан с объектом. Когда вы видите D (A)#foo, это значит что D унаследовал метод #foo от А. Если вы вызовите #foo на объекте который включает D, вы получите метод, определенный в А.

К слову об объектах, почему мы до сих пор не сделали ни одного? Что хорошего от мешка с методами, если нет объекта, к которому мы могли бы его применить. Что же это то место, где Class вступает в действие. В руби класс — это подкласс модуля, что звучит странно, однако помните, что все они структуры данных, которые хранят методы. Класс — почти как модуль, с той точки зрения, что он хранит методы и может включать в себя другие модули, но имеет некоторые дополнительные возможности. Одна из которых — это возможность создавать объекты.

class K
  include D
end

k = K.new

Мы снова имеем возможность определить откуда берутся методы объекта.

>> k.method(:start)
=> #

Это показывает, что когда мы вызываем k.start, мы получаем #start метод из модуля С. Вы обратите внимание, что когда вы вызываете instance_method модуля, это возвращает UnboundMethod, а в случае с объектом Method. Разница в том, что Method связан с объектом. Когда вы вызовите #call у объекта, поведение будет таким же как и в случае с k.start. UnboundMethods не могут быть вызваны непосредственно, т.к. они не имеют объекта который мог бы их вызвать.

Это выглядит так, что мы ищем метод начиная с класса, которому объект принадлежит, затем просматриваем всю цепочку наследования, пока не найдем место определения метода. Что ж, это почти что правда, но в руби есть другой трюк в рукаве: singleton методы. Вы можете добавить новые методы к объекту, и только к этому объекту, без добавления его в класс. Смотрите:

>> def k.mart ; end

>> k.method(:mart)
=> #.mart>

Мы также можем добавлять их к модулям, т.к. модули это всего лишь один из типов объекта.

>> def B.roll ; end

>> B.method(:roll)
=> #

Если в названии метода есть (.) вместо хеша (#), это означает что метод существует только для этого объекта, вместо того, чтобы находиться в модуле. Однако ранее мы сказали, что руби использует модули для хранения методов; простые старые объекты не имели такой возможности. Так где же singleton методы хранятся?

Каждый объект в руби (и запомните, модули и классы это тоже объекты) имеют, так называемые метаклассы, также известные как singleton классы, eigenclass или виртуальные классы (virtual class). Работа этих классов — просто хранить singleton методы объекта. Изначально, они не содержат никаких методов и имеют класс объекта, как своего единственного родителя. Так что для нашего объекта k, цепочка наследования будет выглядеть следующим образом:

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

Мы можем попросить руби показать метакласс объекта. Здесь мы видим что метакласс, это анонимный класс привязанный к объекту k, и он имеет instance метод #mart которого не существует в K классе.

>> k.singleton_class
=> #>

>> k.singleton_class.instance_method(:mart)
=> #>#mart>

>> K.instance_method(:mart)
NameError: undefined method `mart' for class `K'

Один момент на который стоит обратить внимание — это то что метакласс не отображается в цепочке наследования, однако вы должны понимать что он всеравно участвует в цепочке поиска места где метод определен.

Когда мы вызываем метод объекта k, объект спрашивает свой метакласс, не содержит ли он этот метод и метакласс дальше просматривает цепочку наследования с целью определения места нахождения метода. Singleton методы находятся в метаклассе и они имеют преимущество над методами определенными в классе объекта и всеми его родителями.

Сейчас, мы подходим ко второму специальному свойству класса, помимо их способности создавать объекты. Классы имеют специальную форму наследования, называемую «субклассирование» (subclassing). Каждый класс имеет один и только один суперкласс, по умолчанию это Object. С точки зрения вызова метода, вы можете думать о суперклассах, как о первом родительском модуле класса:

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

Таким образом, цепочка наследования дает нам [Foo, Extras, Bar], в обоих случаях, и она, как это было ранее, определяет порядок поиска метода. (В действительности это выглядит так [Foo, Extras, Bar, Object, Kernel, BasicObject], но мы рассмотрим это через минуту). Заметьте, что руби нарушает Liskov принцип замещения, не позволяя классам быть включенными (include), только модули могут быть использованы подобным образом, а не их подтипы. Продемонстрированный выше сниппет, показывает как субклассирование влияет на порядок поиска метода, и код справа не будет работать, если Bar является классом.

Если субклассирование это то же самое что и включение (include), зачем нам нужны обе эти особенности? Что ж, это дает нам ещё одну возможность. Классы наследуют singleton методы их суперклассов, а в случае с модулями этого не происходит.

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
>> Z.z
=> :z
>> Foo.z
NoMethodError: undefined method `z' for Foo:Class

Мы можем смоделировать это в плане родительских отношений, сказав что метаклассы субклассов имеют метаклассы суперклассов, как своих родителей.

    +-----+         +--------------+
    | Bar +~~~~~~~~>| # |
    +-----+         +--------------+
       ^                    ^
       |                    |
    +--+--+         +-------+------+
    | Foo +~~~~~~~~>| # |
    +-----+         +--------------+

И действительно, если мы посмотрим на Foo, мы увидим что его #bar метод происходит из метакласса Bar.

>> Foo.method(:bar)
=> #

>> Foo.singleton_class.instance_method(:bar)
=> ##bar>

Мы увидели, как наследование и порядок поиска метода в руби, может быть изображен как дерево модулей, с включением и субклассированием, создающим различные родительские взаимоотношения. Мы также объяснили одиночное и множественное наследование методов объектов и singleton методов. Теперь давайте посмотрим на несколько вещей, что находятся на закорках этой модели.

Первое — это Object#extend метод. Вызывая object.extend (M), мы делаем методы из модуля М доступными в объекте. Мы не копируем методы, Мы просто добавляем М как родителя метакласса данного объекта. Если объект имеет класс Thing, мы получаем следующее отношение:

                     +-------+      +-----+
                     | Thing |      |  M  |
                     +-------+      +-----+
                         ^             ^
                         +-------+-----+
                                 |
    +--------+         +---------+-------+
    | object +~~~~~~~~>| # |
    +--------+         +-----------------+

Таким образом, расширение объект при помощи модуля — это тоже самое, что и включение этого модуля в метакласс объекта. (На самом деле существует некоторая разница, но это не относится к данной теме). Глядя на это дерево, мы видим, что когда мы вызываем метод объекта, система диспетчеризации методов, предпочтет методы, определенные в модуле М, тем, что находятся в Thing (будет использован метод из М), и, в свою очередь, методы находящиеся в метаклассе объекта, будут приоритетнее чем M и Thing.

Этот контекст важен: мы не можем сказать что методы в М имеют приоритет над Thing в общем смысле, а только в том случае, когда мы говорим о вызове метода объекта. Цепочка наследования, где ищется метод — вот что важно. И это проявляется когда мы исследуем работу super. Взгляните на следующий набор модулей:

module X
  def call ; [:x] ; end
end

module Y
  def call ; super + [:y] ; end
end

class Test
  include X
  include Y
end

Цепочка наследования для Test следующая: [Test, Y, X], так что если мы вызовем Test.new.call, мы вызовем #call метод из Y. Но, что произойдет когда Y вызовет super? Y не имеет своей собственной цепочки наследования, тоесть нет никого, на ком Y мог бы вызвать этот метод, правильно?

А вот и нет. Когда мы столкнулись с вызовом super, что важно, это то что мы сделали вызов метода для цепочки наследования объекта, вот и все. Вы можете представить себе поиск метода, как поиск всех определений данного метода в цепочки наследования метакласса объекта.

>> t = Test.new

>> t.singleton_class.ancestors.map { |m|
     m.instance_methods(false).include?(:call) ? m.instance_method(:call) : nil
   }.compact

=> [#, #]

Чтобы определить место нахождения метода, мы вызываем первый метод в цепочке наследования. Если этот метод вызывает super, мы прыгаем до следующего, и так далее, пока мы не закончим поиск. Если бы Test не включал в себя модуль Х, не было бы никакой имплементации #call кроме той что определена в Y, таким образом, вызов super привел бы к ошибке.

В самом деле, в нашем случае Test.new.call вернет [: x, : y].

Мы почти закончили, но я обещал рассказать что такое Object, Kernel и BasicObject. BasicObject — это корневой класс всей системы; это класс без суперкласса. Object наследуется от BasicObject, и является базовым суперклассом для всех классов пользователя. Различие между ними в том, что BasicObject почти не имеет методов, в то время как Object имеет достаточно много: методы ядра руби, такие как: #==, #__send__, #dup, #inspect, #instance_eval, #is_a?, #method, #respond_to?, and #to_s. Хотя, на самом деле, Object сам не содержит все эти методы, он получает их из Kernel. Kernel — это просто модуль с набором всех методов объекта из ядра руби. Поэтому, если мы попытаемся отобразить систему объектов ядра руби, мы получим следующее:

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

Эта диаграмма показывает модули и классы ядра руби: BasicObject, Kernel, Object, Module и Class, их метаклассы и как все они связаны. Да, BasicObject.singleton_class.superclass — это Class. Руби делает небольшую вуду магию, чтобы завести эту шарманку (прим. переводчика). В любом случае, если вы хотите понять диспетчеризацию методов в руби, просто запомните:

  • Модуль — это мешок с методами
  • Модуль может иметь много родителей
  • Класс — это модуль который может создавать объекты
  • Каждый объект имеет метакласс, который имеет класс объекта как своего родителя
  • Под субклассированием подразумевается связывание двух классов и их метаклассов
  • Методы ищутся через погружение в дерево «родословной», просматривая ветки справа налево

Нет, я не знаю всех тонкостей этой работы. Никто не знает.

Оригинальная статья: blog.jcoglan.com/2013/05/08/how-ruby-method-dispatch-works

© Habrahabr.ru