1. Metaprogramming patterns — 25кю. Метод eval

    Программирование, которым я периодически по-прежнему занимаюсь, постепенно меняет свой стиль и всё больше связано с метапрограммированием. При этом нельзя сказать, что обычное программирование мне опостылело. Просто как любой программист, я ищу пути для всё большей модульности, краткости, внятности и гибкости кода, и в метапрограммировании мне видится нераскрытый потенциал (несмотря на давний необозримый интернетовский флуд по метапрограммированию идущий ещё от Lisp). :)

    Хочу начать вести блог, посвященный метапрограммированию на Ruby.

    Выбор Ruby связан с тем, что культура метапрограммирования в среде Ruby-программистов уже во многом сформировалась, и элементы метапрограммирования стали тканью повседневного труда Ruby-программиста, и, кроме того, он мне лучше известен, чем другие динамические языки.

    Я читал и читаю лекции по Ruby & Rails & Metaprogramming на Физтехе; материалы одной из лекций можно взять здесь. Там коротко о главном в картинках.

    В этом блоге я постараюсь излагать тему последовательно и подробно. Заранее делаю глубокий вдох, потому как поставленная задача не простая. Надеюсь на ваш подбадривающий фидбэк.

    Начну с простого — с определения.

    Метапрограммирование в скриптовых языках — это стиль написания программ, при котором с пользой используются возможности изменения пространства имен в runtime.

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

    Надо сказать, что в большинстве скриптовых языков пространство имен конструируется не иначе как в режиме runtime. Но многие об этом не помнят, поэтому я это подчеркнул в определении. Если убрать из определения лишнее упоминание про runtime, то останется словосочетание «с пользой». Значит в нём и суть.

    Пример безполезного неметапрограммирования: eval "s = 'eval s'; eval s"

    Калькулятор



    Помню как в глубоком детстве я писал для БК-0010 программу построения графиков функций. Функции хардкодились и при работе программы можно было лишь выбрать одну функцию из списка и указать диапазон [x0, x1], а диапазон по оси Y (о чудо программистской мысли! способной автоматизировать всё и вся) выбирался программой автоматически.

    Смотрел я на свою программу на Бейсике и переживал экстаз. Но тут меня посетила грустная мысль: «Эх!!! А жаль все таки, что нельзя прямо во время выполнения программы вбить формулу нужной мне функции.»

    Ндаа… 8-й класс, 1992 год, г. Кирово-Чепецк. Много с той поры воды утекло, а проблемы всё те же!

    К чему это я?

    Вот вам код интерактивного «калькулятора» на языке Ruby:

    while line = readline
      puts eval(line).inspect
    end
    

    или получше
    while (print "> "; true) and line = readline
      puts eval(line).inspect
    end
    

    или правильно
    require 'readline'
    while line = Readline.readline("> ")
      begin 
        puts eval(line).inspect
      rescue => e
        puts e.to_s + "\n" + e.backtrace.join("\n")
      end
    end


    Пример выполнения:
    artem@laptop:~/meta-lectures$ ruby console.rb 
    > 1+2
    3
    > "hello"
    "hello"
    > def fib(n) (0..1)===n ? 1 : fib(n-1)+fib(n-2) end
    nil
    > (0...10).map{|n| fib(n)}
    [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    > 1/0
    (eval):1:in `/': divided by 0
    console.rb:4
    (eval):1  
    > exit
    artem@laptop:~/meta-lectures$
    

    В скриптовых языках есть метод eval, который получает строку и выполняет эту строку в текущем контексте так (почти так), как если бы она была написана программистом в месте вызова eval.

    Собственно, средства, подобные eval, есть и в компилируемых языках программирования.

    Кстати, метод eval я бы не относил к метапрограммированию и даже назвал бы его крайне вредным методом для этого занятия. Интерактивный ruby|python|perl|...-shell — пожалуй, один из немногих примеров, где его стоит применять. О вреде метода eval поговорим далее.

    attr_accessor



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

    Смысл выражения attr_accessor выведите из следующего утверждения: код
    class Song
      attr_accessor :title, :length
    end

    равносилен (по результату) коду
    class Song
      def title
        @title
      end
      def title=(v)
        @title = v
      end
      def length
        @length
      end
      def length=(v)
        @length = v
      end
    end

    Вот вам и определение!

    Невооруженным взглядом видно, что attr_accessor полезен, так как отвечает внутреннему стремлению программиста сделать код кратким и внятным. Конструкцию attr_accessor можно переводить как «Хочу set- и get- методы для следующих атрибутов экземпляров класса».

    Конструкция attr_accessor вовсе даже не неведомый зверь (читай — не есть встроенная конструкция языка), а обычный метод, который можно запрограммировать самим. Давайте это сделаем с помощью метода eval.

    def attr_accessor(*methods)
      methods.each do |method|
        eval %{
          def #{method}
            @#{method}
          end
          def #{method}=(v)
            @#{method} = v
          end
        }
      end
    end

    Теперь attr_accessor, получая в качестве аргумента массив названий атрибутов, для каждого названия выполняет строчки кода, определяющие соответствующие атрибуту set- и get- методы.

    Возможность писать методы, подобные attr_accessor появилась потому, что в Ruby нет понятия определения класса или метода. Написав строку "class Song" мы просто перешли в некоторый новый контекст, в котором можно заниматься обычными вычислениями, и конструкция "def xxx() ... end" лишь одно из выражений, результат вычисления которого всегда равен nil (в ruby v1.8), а сторонний эффект проявляется в появлении метода "xxx" у класса, в контексте которого эта конструкция выполнилась.

    Определение функции выполнилось? Перешли в контекст класса? Что за бред? — спросит неизвестно откуда взявшийся здесь программист С++. Да, именно так.

    "class Song" не обрамляет спереди определение класса, а осуществляет переход в специальный контекст, в котором меняется область видимости пространства имен; то есть появляются некоторые новые методы, которые мы можем вызвать в данном контексте, меняются значения и эффект от выполнения некоторых инструкций и т.д. и т.п.

    Текст "def xxx() ... end" действительно является выражением и выполняется виртуальной машиной Ruby. При этом внутренность определения метода не выполняется, а транслируется в байт код и запоминается под именем метода.

    Q: Что значит контекст класса?


    A: Это такой контекст, в котором выражение self равно некоторому классу.

    Выполните следующий код:
    puts "hi1 from #{self.inspect}"
    class Abc
      puts "hi2 from #{self.inspect}"
      def hi
        puts "hi3 from #{self.inspect}"
      end
    end

    Будут напечатаны строки с hi1 и hi2. Строку с hi3 вы увидите, если допишете
    Abc.new.hi
    

    Итого получите:
    artem@laptop:~/meta-lectures$ ruby self_in_contexts.rb 
    hi1 from main
    hi2 from Abc
    hi3 from #<Abc:0xb7c3d9dc>
    artem@laptop:~/meta-lectures$
    

    Надо понимать, что когда вы пишете
    my_method(arg1,arg2)
    

    то по сути спереди неявно подставляется "self.":
    self.my_method(arg1,arg2)
    


    Но эти два выражения не эквивалентны в некоторых случаях.
    Например, когда my_method является private-методом, то выражение self.my_method даст ошибку вызова private метода. Это особенности реализации Ruby — private-методы и есть такие методы, которые нельзя вызывать через точку.


    Ладно, хватит разглагольствовать. Исправим указанный выше код attr_accessor, чтобы он стал работающим:
    class Module
      def attr_accessor(*methods)
        methods.each do |method|
          class_eval %{
            def #{method}
              @#{method}
            end
            def #{method}=(v)
              @#{method} = v
            end
          }
        end
      end
    end

    Что мы сделали? Мы поместили определение метода в контекст класса Module и заменили eval на class_eval.

    Почему мы так сделали? Есть причины:
    * Нехорошо писать методы без понимания того, для каких объектов они будут доступны. Нам нужно написать метод attr_accessor, который можно использовать в контексте классов (экземпляров класса Class) и модулей (экземпляров класса Module). Класс Class наследует от класса Module, поэтому достаточно определить этот метод как метод экземпляров Module, тогда он будет доступен как для модулей так и для классов.
    * Метод class_eval имеет свои отличия от eval, в частности последний при выполнении выражения "def ... end" будет создавать определение метода локально живущего внутри метода attr_accessor и доступного только во время выполнения метода attr_accessor (это незадокументированная фича "def внутри def"). Метод class_eval выполняет заданный код в правильном контексте, так что "def" начинают приводить к нужному результату. Метод class_eval активно и используется в метапрограммировании именно в варианте, когда его аргументом является блок, а не строка.

    Итак, теперь код работает. Но он неправильный. Есть и другие неправильные решения, в том числе без "class Module" и "class_eval". Вот одно из них:

    def attr_accessor(*methods)
      methods.each do |method|
        eval %{
        class #{self}
          def #{method}
            @#{method}
          end
          def #{method}=(v)
            @#{method} = v
          end
        end
        }
      end
    end


    Последний вариант плох тем, что его можно вызвать не в контексте класса и получить что-то нехорошее, зависящее от того, чему в этом контексте равно выражение self. Например:
    s = "Class"
    s.instance_eval { attr_accessor :hahaha}
    Array.hahaha = 3    # неожиданным образом у Array  появился атрибут hahaha
    puts Array.hahaha   #


    САМОЕ ВАЖНОЕ:
    Описанные определения attr_assessor с использованием eval плохи своей несекьюрностью — они не защищены ни от злого умысла врага, ни от глупости самого программиста: если значение переменной method не является валидной строкой для имени метода, а например, равно строке "llalala(); puts `cat /etc/passwd`; puts ", то последствия будут непредсказуемые. Никаких ошибок (исключений) при выполнении программы вы можете и не увидеть; сюрпризы полезут лишь тогда, «когда ракета будет уже лететь» (с). Нет же ничего хуже ошибок, которые проявляются с запозданием, когда концов уже не сыщешь.

    Напишем, наконец то, правильный вариант определения attr_accessor. Он, в отличие от неправильных, единственен:

    class Module
      def attr_accessor(*methods)
        methods.each do |method|
          raise TypeError.new("method name  is not symbol") unless method.is_a?(Symbol)
          define_method(method) do
            instance_variable_get("@#{method}")
          end
          define_method("#{method}=") do |v|
            instance_variable_set("@#{method}", v)
          end
        end
      end
    end


    attr_accessor с дефолтным значением


    Мы часто пишем атрибуты с дефолтным значением. Делаем мы это, используя идиому "||=", которая грубо переводится как «инициализировать то, что слева, тем, что справа, если оно ещё не инициализировано»:

    class Song
      def length
        @length ||= 0
      end
      def title
        @title ||= "no title"
      end
    end
    Song.new.length #=> 0 
    Song.new.title  #=> "no title" 
    

    после такого определения, значение атрибута length новой песни будет равно 0.

    По щучьему веленью, по моему хотенью,… пусть данный код работает так, как я хочу!!!:
    class Song
      attr_accessor :length, :default => 0
      attr_accessor :title,  :default => "no title"
    end
    

    Напишем исключительно в учебных целях неправильный код, использующий class_eval от строки:
    class Module
      def attr_accessor(*methods)
        options = methods.last.is_a?(Hash)? methods.pop: {}
        methods.each do |method|
          class_eval %{
            def #{method}
              \# не пишите так никогда!
              @#{method} #{ "||= #{options[:default]}" if options[:default] }
            end
            def #{method}=(v)
              @#{method} = v
            end
          }
        end
      end
    end

    Да будет чудо!!!
    class Song
      attr_accessor :length, :default => 42
    end
    puts Song.new.length # выводит 42!!!
    


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

    При выполнении
    class Song
      attr_accessor :length, :default => 42
      attr_accessor :title, :default => "no title"
    end
    puts Song.new.length # выводит 42!!!
    puts Song.new.title # oooooops!!!


    получаем загадочное:
    artem@laptop:~/meta-lectures$ ruby bad_attr_accessor.rb 
    42
    (eval):5:in `title': stack level too deep (SystemStackError)
    	from (eval):5:in `title'
    	from bad_attr_accessor.rb:27
    artem@laptop:~/meta-lectures$ 
    

    Почему возникла такая неприятность? Дело в том, что есть фундаментальная проблема: вставки внутрь строки некоторых объектов просто невозможны.

    Правильно задача об attr_accessor с дефолтным значением решается так:

    class Module
      def attr_accessor(*methods)
        options = methods.last.is_a?(Hash)? methods.pop: {}
        methods.each do |method|
          raise TypeError.new("method name is not symbol") unless method.is_a?(Symbol)
          define_method(method) do
            instance_variable_get("@#{method}") ||
              instance_variable_set("@#{method}", options[:default])
          end
          define_method("#{method}=") do |v|
            instance_variable_set("@#{method}", v)
          end
        end
      end
    end


    Итак, в рассмотренных примерах метапрограммирование выглядит как написание методов определяющих методы.

    Начинающему метапрограммисту имеет смысл погуглить такие поисковые запросы:
    1. ruby doc attr_accessor
    2. ruby doc Kernel eval
    3. ruby doc Module class_eval
    4. ruby doc Object instance_eval
    5. ruby doc Object is_a?
    Первые ссылки верны.

    Как метапрограммировать без eval, а также о примесях, о методах модификаторах, позволяющих перевести на новый уровень абстракции задачи, связанные с кешированием, RPC, DSL, о паттернах, продолжающих идеи отложенных (ленивых) вычислений и др. читайте в следующих выпусках блога.
    Поделиться публикацией
    Комментарии 12
    • НЛО прилетело и опубликовало эту надпись здесь
        +1
        А как здесь это делают? Постят подсвеченый html или есть специальные теги?
        • НЛО прилетело и опубликовало эту надпись здесь
            +1
            Стал использовать vim. Там есть необходимая фича, которая выдает HTML, который не портится хабрахабровским обработчиком. Но ой как сильно мне бы хотелось писать код так:
            <source lang="ruby">
             puts "Hello"
            </source>
            

            Это же просто сделать. Алё, админы!
        +1
        Старые знакомые :). Привет.
          +1
          attr_accessor :title, :length
            0
            У меня мысль такая
            @#{method} #{ "||= #{options[:default]}" if options[:default] }
            Не работает потому, что в случае attr_accessor :title, :default => "no title" получается следующий код:
            class Song
            def title
            # дай вам бог понять, что тут написано
            # не пишите так никогда!
            @title ||= no title
            end

            def title=(v)
            @title = v
            end
            end

            Если сделать @#{method} #{ "||= #{options[:default].inspect}" if options[:default] }
            То вариант attr_accessor :title, :default => "no title" заработает.
              0
              Да, верно. Но есть такие объекты для которых inspect возвращает нечто, не являющееся Ruby выражением, равное им
              $ ruby -e  "puts lambda{|x| x*x}.inspect"
              #<Proc:0xb7d17df8@-e:1>
              

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

                +1
                Поэтому я и написал «То вариант attr_accessor :title, :default => „no title“ заработает. „
              +1
              Статью нужно было назвать «Культура метапрограммирования» или «О вреде eval»:)
                0
                А можно вот так вот определять новые методы классу?

                class Test

                end

                Test.class_eval { def rest; puts 'hello'; end }

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

              Самое читаемое