Я ненавижу константы в Ruby

habr.png

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


Мини-объяснение

Кратко говоря, поиск констант происходит в несколько этапов:


  1. Поиск в т.н. лексической области видимости. Т.е. поиск будет происходить в зависимости от того, в каком месте определена текущая строчка кода. Например, в самом первом выводе интерпретатор находится на верхнем уровне (top-level) и выводит константу Namespace::C::A, а во втором выводе он сначала входит в модуль Namespace, потом входит в класс C и только тогда делает puts. Подробнее об этом можно узнать, почитав про вложенность (nesting), в частности, метод Module.nesting.
  2. Если первый этап не был успешным, то интерпретатор начинает «опрос» миксинов и родительских классов. Для каждого из опрошенных на первом этапе модулей.
  3. Если предыдущий этап не дал результатов, проверяется верхний уровень (top-level).
  4. На этом этапе константа считается ненайденной и вызывается метод 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
  • ?

© Habrahabr.ru