Ruby — очень сложный язык программирования. Он невероятно красивый и читабельный, однако у него есть множество тем и особенностей, которые могут оставаться "темным лесом" даже для опытного Ruby-разработчика. Одной из таких тем является поиск констант.
Несмотря на заголовок, гнева в посте не будет.
Целью этого поста не является детальное объяснение алгоритма поиска. Я бы сказал, что целью является привлечение внимания разработчиков к теме. Отчасти, это просто крик души.
Пример
Я рассмотрю один небольшой пример. Для начала определим несколько констант:
module M
A = 'm'
end
module Namespace
A = 'ns'
class C
include M
end
end
У нас есть один миксин M
, модуль Namespace
и принадлежащий ему класс C
. В модулях определенно по константе A
, которые мы и будем искать.
Как думаете, что выведет следующий код? Я помещу ответы ниже, чтобы они не бросались в глаза.
puts Namespace::C::A
module Namespace
class C
puts A
end
end
Теперь давайте определим пару методов:
module M
def m
A
end
end
module Namespace
class C
def f
A
end
end
end
class Namespace::C
def g
A
end
end
x = Namespace::C.new
puts x.f
puts x.g
puts x.m
Как думаете, есть ли между ними разница?
Ответы
Вот полный код нашего примера с ответами в комментариях:
module M
A = 'm'
end
module Namespace
A = 'ns'
class C
include M
end
end
puts Namespace::C::A # m
module Namespace
class C
puts A # ns
end
end
module M
def m
A
end
end
module Namespace
class C
def f
A
end
end
end
class Namespace::C
def g
A
end
end
x = Namespace::C.new
puts x.f # ns
puts x.g # m
puts x.m # m
Т.е. выводом программы будет:
m
ns
ns
m
m
Мини-объяснение
Кратко говоря, поиск констант происходит в несколько этапов:
- Поиск в т.н. лексической области видимости. Т.е. поиск будет происходить в зависимости от того, в каком месте определена текущая строчка кода. Например, в самом первом выводе интерпретатор находится на верхнем уровне (top-level) и выводит константу
Namespace::C::A
, а во втором выводе он сначала входит в модульNamespace
, потом входит в классC
и только тогда делаетputs
. Подробнее об этом можно узнать, почитав про вложенность (nesting), в частности, методModule.nesting
. - Если первый этап не был успешным, то интерпретатор начинает "опрос" миксинов и родительских классов. Для каждого из опрошенных на первом этапе модулей.
- Если предыдущий этап не дал результатов, проверяется верхний уровень (top-level). На самом деле, можно опустить этот пункт, т.к. он по сути включается во второй, т.к. top-level — это класс
Object
- На этом этапе константа считается ненайденной и вызывается метод
const_missing
по аналогии сmethod_missing
. Полагаю, этот метод и утилизируется в Ruby on Rails для автозагрузки и перезагрузки кода.
Таким образом:
# Мы на верхнем уровне.
# На первом этапе проверяется только С
# На втором этапе константа находится внутри M
puts Namespace::C::A # m
module Namespace
class C
# Мы в Namespace -> Namespace::C
# На первом этапе константа находится внутри Namespace
puts A # ns
end
end
module M
def m
# Мы находимся внутри M. На первом же этапе константа найдена
A # m
end
end
module Namespace
class C
def f
# Мы находимся в Namespace -> Namespace::C
A # ns
end
end
end
class Namespace::C
def g
# Мы находимся в Namespace::C (в модуль Namespace мы не входили)
# Первый этап не увенчается успехом
# На втором этапе мы находим нужную константу в миксине
A # m
end
end
Заключение
Можно сказать, Ruby заставляет нас при написании в коде констант вычислять их значение относительно написанного кода, а не относительно контекста выполнения (очень странно звучит, простите).
Ruby style guide определяет одно хорошее правило:
определять и переоткрывать вложенные классы/модули нужно явным образом. Т.е. никогда не нужно писать class A::B
. Этого простого правила достаточно, чтобы избегать сюрпризов и в большинстве случаев не задумываться о поиске констант вовсе.
Что можно почитать:
- Глава 7.9 книги "The Ruby Programming Language" — чтобы узнать все из первых рук, как говорится.
- Гайд по автозагрузке констант в Rails
- Ruby style guide
- Не читать, но поиграться с Module.nesting
- ??
Update
Пользователь DsideSPb дал полезный комментарий о дополнительной особенности поиска констант. Правда, она была удалена в последнем (2.5.0) релизе.
Лично я не знаю всех деталей, но при некоторых обстоятельствах при указании неправильного пути до константы интерпретатор может подменить ее на таковую из top-level. Однако работает это далеко не во всех случаях:
# 1.rb
class A; end
class B; end
A::B # вернет B, но выдаст предупреждение
# 2.rb
class A; end
module M; end
A::M # ==> M с предупреждением
M::A # ==> NameError
Комментарии (10)
l1tero
22.01.2018 23:11Эм… Если честно, то я не понял в чем проблема? Переопределение констант — плохая идея изначально и вполне логично то, что при переопределении они зависят от выполняемого кода.
Nondv Автор
22.01.2018 23:14ни о каком переопределении речь не идет. Где Вы это в посте увидели?
Речь идет о коллизии имен, которая происходит потому, что в Ruby не обязательно указывать полный путь до константы.
module M1 A = :m1 module M2 A = :m2 end end M1::A # ==> :m1 M1::M2::A # ==> :m2 module M1 puts A # which one? module M2 puts A # and this? end end
l1tero
22.01.2018 23:38module M A = 'm' end module Namespace A = 'ns' class C include M end end
Ну как по мне, то include как раз и переопределит одноименную константу, указанную выше, что как мне кажется вполне логично.Nondv Автор
22.01.2018 23:54module M A = 'm' end module Namespace A = 'ns' class C include M puts A end end
Выведет
ns
. Если бы Вы внимательно прочитали пост, то знали бы это ;)
Опять же, не используйте слово "переопределит". В английском языке это называется, если не ошибаюсь, "shadowing", т.е. они могут "перекрывать" друг друга. Константы
Namespace::A
иM::A
друг другу не мешают. Вопрос в том, на какую из них будет ссылаться простоA
DsideSPb
23.01.2018 00:34+1Забавный факт: в недавно (месяц назад) выпущенном Ruby 2.5 кусок алгоритма, представленный вами в п. 3 ("проверяется верхний уровень") отпилен.
Nondv Автор
23.01.2018 00:50Прекрасное дополнение! Совсем вылетело это из головы.
Однако, это не совсем то, о чем я говорил (хотя стоило это добавить).
Фича, которую выпилили, подменяла константу в случае, если был указан неправильный путь к ней. Причем, там все достаточно хитро. Например:
# 1.rb class A; end class B; end A::B # вернет A, но выдаст предупреждение # 2.rb class A; end module M; end M::A # ==> NameError
Эта фича срабатывает только при явных ошибках в коде (явное указание неправильного пути к константе) и действительно является поводом ненависти:) Хотя лично я проблемы с ней встречал всего пару раз в каком-то легаси коде.
Anyway, спасибо огромное за ценный комментарий
Nondv Автор
А еще я ненавижу маркдаун хабра (хабрдаун? О__О), который заставляет меня удалять переносы строк :(