Метапрограммирование на языке Ruby

Язык Ruby предоставляет богатый набор возможностей для метапрограммирования.

Манипулирование значениями переменных, удаление переменных

  • Object#instance_variable_get
  • Object#instance_variable_set
  • Object#remove_instance_variable
  • Module#class_variable_get
  • Module#class_variable_set
  • Module#remove_class_variable

Манипулирование значениями констант, удаление констант 

  • Module#const_get Module#const_set
  • Module#remove_const

Добавление/удаление методов  

  • Module#define_method
  • Module#remove_method

Запуск динамически генерируемого кода 

  • Object#send
  • Object#instance_eval
  • Module#module_eval
  • Kernel#eval
  • Kernel#method_missing

Reflection-методы (группа методов, позволяющих исследовать структуру кода в момент исполнения) 

  • Object#class
  • Object#instance_variables
  • Object#methods
  • Object#private_methods
  • Object#public_methods
  • Object#singleton_methods
  • Module#class_variables
  • Module#constants
  • Module#included_modules
  • Module#instance_methods
  • Module#name
  • Module#private_instance_methods
  • Module#protected_instance_methods
  • Module#public_instance_methods

Вычисление строк и блоков Ruby предоставляет метод eval, позволяющий в runtime превратить в код строку или другой вычисляемый блок. Для того, чтобы сгенерировать некоторый код в контексте некоторого созданного объекта следует использовать методы instance_eval and module_eval (синоним class_eval). Метод instance_eval должен вызываться в пространстве имен созданного объекта:

[1,2,3,4].instance_eval('size') # returns 4

В предыдущем примере строка ‘size’ интерпретируется в контексте объекта-массива, что эквивалентно следующей записи:

[1,2,3,4].size

Методу instance_eval можно передать Ruby-блок кода:

# Get the average of an array of integers
[1,2,3,4].instance_eval { inject(:+) / size.to_f } # returns 2.5

Методы inject(:+) и size.to_f использованы без явного указания объекта, относительно которого они вызваны. Это возможно потому, что контекстом их выполнения является объект-массив, относительно которого вызван instance_eval (то есть self). В то время, как instance_eval вычисляет выражение в контексте созданного объекта, метод module_eval вычисляет код в контексте класса Module или Class:

Fixnum.module_eval do
  def to_word
    if (0..3).include? self
      ['none', 'one', 'a couple', 'a few'][self]
elsif self > 3 'many' elsif self < 0 'negative' end end end 1.to_word # returns 'one' 2.to_word # returns 'a couple'

В примере выше метод module_eval контекст класса Fixnum и добавляет новый метод. Этот код выполняет то же самое, что и код:

class Fixnum
  def to_word
    ..
  end
end

Наибольшую гибкость этот подход даёт при использовании строк, содержащих динамически генерируемые имя/тело метода. В следующем примере создаваемый статический метод create_multiplier добавляется в класс Fixnum:

class Fixnum
  def self.create_multiplier(name, num)
module_eval "def #{name}; self * #{num}; end" end end Fixnum.create_multiplier('multiply_by_pi', Math::PI)
4.multiply_by_pi # returns 12.5663706143592

В данном примере создаётся метод класса (или метод ‘singleton’)Ю при вызове которого, создаются методы инстанции, используемые любым объектом Fixnum.

Использование метода send

Использование метода send аналогично использованиию метода instance_eval с той разницей, что он передает объету имя вызываемого метода. Это особенно полезно когда имя метода формируется как строка или является символом (‘size’, :size):

method_name = 'size'
[1,2,3,4].send(method_name) # returns 4

Особенность метода состоит в том, что он вызывается в обход системы контроля доступа к методам и может вызывать private-методы, например, Module#define_method:

Array.define_method(:ducky) { puts 'ducky' }  
# NoMethodError: private method `define_method' called for Array:Class

При использовании send:

Array.send(:define_method, :ducky) { puts 'ducky' }

Добавление методов

Для добавления методов в класс следует использовать метод define_method:

class Array
  define_method(:multiply) do |arg|
collect{|i| i * arg}
end end [1,2,3,4].multiply(16) # returns [16, 32, 48, 64]

Использование метода method_missing

Если у объекта класса вызывается метод, ранее не определенный в теле класса, то вызывается метод method_missing, которому передается имя этого неизвестного метода. В method_missing появляется возможность обработать отсутствие метода в зависимости от имени вместо получения стандартной ошибки NoMethodError:

class Fixnum
  def method_missing(meth)
method_name = meth.id2name
if method_name =~ /^multiply_by_(\d+)$/ self * $1.to_i
else raise NoMethodError, "undefined method `#{method_name}' for #{self}:#{self.class}" end end end 16.multiply_by_64 # returns 1024 16.multiply_by_x # NoMethodError

Как работает attr_accessor

Динамическая генерация кода методов используется во всем используемом используемый методе attr_accessor, которые генерирует переменную и метод-getter и метод-setter для неё. В качестве примера создадим собственные методы attr1 и attr2. Так как все классы наследуются от класса Module, то поместим методы в него:

class Module
     def attr1(symbol)
            instance_var = ('@' + symbol.to_s)
            define_method(symbol) { instance_variable_get(instance_var) }  
     define_method(symbol.to_s + "=") { |val| instance_variable_set(instance_var, val) }
     end
     def attr2(symbol)
          module_eval "def #{symbol}; @#{symbol}; end"
          module_eval "def #{symbol}=(val); @#{symbol} = val; end"
     end
end

Пример использования:

class Person
      attr1 :name
      attr2 :phone
end
person = Person.new
person.name = 'John Smith'
person.phone = '555-2344'
person # результат <Person:.... @name="John Smith", @phone="555-2344">

Вызовы методов define_method и module_eval приводят к одинаковым результатам.

Подготовлено по материалам weare.buildingsky.net

Вам также может помочь