Pull to refresh

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

Reading time10 min
Views22K
Программирование, которым я периодически по-прежнему занимаюсь, постепенно меняет свой стиль и всё больше связано с метапрограммированием. При этом нельзя сказать, что обычное программирование мне опостылело. Просто как любой программист, я ищу пути для всё большей модульности, краткости, внятности и гибкости кода, и в метапрограммировании мне видится нераскрытый потенциал (несмотря на давний необозримый интернетовский флуд по метапрограммированию идущий ещё от 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, о паттернах, продолжающих идеи отложенных (ленивых) вычислений и др. читайте в следующих выпусках блога.
Tags:
Hubs:
+10
Comments12

Articles

Change theme settings