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

    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). На самом деле, можно опустить этот пункт, т.к. он по сути включается во второй, т.к. top-level — это класс Object
    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. Этого простого правила достаточно, чтобы избегать сюрпризов и в большинстве случаев не задумываться о поиске констант вовсе.


    Что можно почитать:



    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
    
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 14
    • +2

      А еще я ненавижу маркдаун хабра (хабрдаун? О__О), который заставляет меня удалять переносы строк :(

      • 0
        Эм… Если честно, то я не понял в чем проблема? Переопределение констант — плохая идея изначально и вполне логично то, что при переопределении они зависят от выполняемого кода.
        • 0

          ни о каком переопределении речь не идет. Где Вы это в посте увидели?


          Речь идет о коллизии имен, которая происходит потому, что в 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
          • –1
            module M
              A = 'm'
            end
            
            module Namespace
              A = 'ns'
              class C
                include M
              end
            end
            

            Ну как по мне, то include как раз и переопределит одноименную константу, указанную выше, что как мне кажется вполне логично.
            • +1
              module M
                A = 'm'
              end
              
              module Namespace
                A = 'ns'
                class C
                  include M
                  puts A
                end
              end

              Выведет ns. Если бы Вы внимательно прочитали пост, то знали бы это ;)


              Опять же, не используйте слово "переопределит". В английском языке это называется, если не ошибаюсь, "shadowing", т.е. они могут "перекрывать" друг друга. Константы Namespace::A и M::A друг другу не мешают. Вопрос в том, на какую из них будет ссылаться просто A

        • 0

          deleted

          • +1

            Забавный факт: в недавно (месяц назад) выпущенном Ruby 2.5 кусок алгоритма, представленный вами в п. 3 ("проверяется верхний уровень") отпилен.

            • +1

              Прекрасное дополнение! Совсем вылетело это из головы.


              Однако, это не совсем то, о чем я говорил (хотя стоило это добавить).
              Фича, которую выпилили, подменяла константу в случае, если был указан неправильный путь к ней. Причем, там все достаточно хитро. Например:


              # 1.rb
              class A; end
              class B; end
              A::B # вернет A, но выдаст предупреждение
              
              # 2.rb
              class A; end
              module M; end
              M::A # ==> NameError

              Эта фича срабатывает только при явных ошибках в коде (явное указание неправильного пути к константе) и действительно является поводом ненависти:) Хотя лично я проблемы с ней встречал всего пару раз в каком-то легаси коде.


              Anyway, спасибо огромное за ценный комментарий

              • 0

                опечатался. A::B вернет B

                • +1

                  Точно. То есть, да, отпилен лишь частично. Похоже, что только для случаев, когда в качестве контекста поиска явно указан класс (а для модулей это и так не работало). Тесткейс.


                  Никак не заставлю себя открыть "Ruby Under a Microscope", там наверняка про это есть :/

              • 0
                а почему бы не взять себе за правило обращаться к константе из топ лвла?
                ::Namespace::C::A
                тогда результат у вас всегда будет ожидаемым
                • +1

                  Почему бы тогда не проверять типы в каждом методе?
                  А лучше — сменить язык на джаву.


                  Это просто неудобно:) В быту знание таких "тонкостей" не так уж часто пригождается, однако кто предупреждает, тот вооружен.

                  • 0
                    проверка типов это уже из другой оперы, хотя для объектов форм полезная штука.
                    я к тому что два двоеточия в начале это же не сложно, зато надежно. особенно если проект большой, особенно если писали его не вы один
                    • +1

                      нет, это абсолютно та же самая ситуация. Вы предлагаете отказаться от динамики в пользу строгости.
                      Иногда это имеет смысл. Но не постоянно.


                      Поэтому и нужно знать язык, на котором пишешь, чтобы понимать, когда нужно написать строже, а когда нет.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.