Что такое «метапрограммирование»?
Метапрограммирование — это вид программирования, связанный с созданием программ, которые порождают другие программы как результат своей работы (в частности, на стадии компиляции их исходного кода), либо программ, которые меняют себя во время выполнения (самомодифицирующийся код). Первое позволяет получать программы при меньших затратах времени и усилий на кодирование, чем если бы программист писал их вручную целиком, второе позволяет улучшить свойства кода (размер и быстродействие) (из Wikipedia).
В этом учебнике перечисляются все методы ядра Ruby, используемые при метапрограммировании и демонстрирующие обобщенные способы применения, которые будут полезны для вас. В завершение, представлен пример разработки динамического класса, работающего с базой данных, наподобие ActiveRecord, который автоматически генерирует классы для таблиц базы данных и заполняет каждый класс модели get/set-методами для ее полей.
Инструменты метапрограммирования
Язык 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 (синоним для Module#class_eval)
Kernel#eval
Kernel#method_missingМетоды для рефлексии
Рефлексия является важной частью метапрограммирования, т.к. позволяет взглянуть на объект и исследовать его содержимое и структуру.
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Вычисление строк и блоков
Нельзя обойти вниманием eval-методы, которые позволяют вычислять строку или блок как код ruby. Когда нужно вызвать
eval в области видимости обычного объекта, можно использовать методы instance_eval и module_eval (синоним для class_eval).Метод
instance_eval работает в области видимости созданного экземпляра объекта.[1,2,3,4].instance_eval('size') # вернет 4В приведенном примере методу
instance_eval передается строка 'size', которая интерпретируется как метод получаемого объекта. Эта запись эквивалентна следующей:[1,2,3,4].sizeМожно также передать блок методу
instance_eval.# вычислим среднее значение массива целых
[1,2,3,4].instance_eval { inject(:+) / size.to_f } # вернет 2.5
# прим.перев.: на ruby 1.8.6 код выдает ошибку LocalJumpError: no block given. Работает такой вариант: [1,2,3,4].instance_eval { inject { |s,e| s + e} / size.to_f }Обратите внимание, как методы
inject(:+) и size.to_f «подвешены в воздухе» без объекта. Это потому, что они вызываются в контексте экземпляра и вычисляются как self.inject(:+) / self.size.to_f, где self — получаемый массив объектов.Если
instance_eval вычисляется для созданного экземпляра объекта, то module_eval вычисляется для модуля или класса.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 # вернет 'one'
2.to_word # вернет 'a couple'На этом примере видно как module_eval переоткрывает существующий класс
Fixnum и добавляет новый метод. В этом нет ничего особенного и аналогичного результата можно добиться другим путем для экземпляра (класса — перев.):class Fixnum
def to_word
...
end
endНо по настоящему выгода проявляется при динамической генерации кода. Мы создадим метод класса
create_multiplier для динамической генерации метода с выбранным нами именем.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 # вернет 12.5663706143592Приведенный пример создает метод класса (синглтон-метод), который при вызове генерирует методы экземпляра, доступные для использования в любом объекте класса Fixnum.
Использование send
Использование send похоже на instance_eval тем, что передается имя метода объекту-получателю. Это полезно, когда нужно динамически получить имя метода из строки или символа
method_name = 'size'
[1,2,3,4].send(method_name) # вернет 4Можно указать имя метода как строку или символ:
'size' или :sizeПотенциальный бонус от использования send — это игнорирование уровня доступа к методу, что позволяет вызывать закрытые методы, такие как 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) # вернет [16, 32, 48, 64]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 # вернет 1024
16.multiply_by_x # NoMethodErrorКак работает attr_accessor?
Большинство из нас использует attr_accessor в наших классах, но не все понимают, что происходит на заднем плане. attr_accessor динамически генерирует get/set-методы для экземпляра переменной. Рассмотрим поближе.
class Person
attr_accessor :first_name
end
john = Person.new
john.first_name = 'John'
john.instance_variables
# вернет ["@first_name"]
john.methods.grep /first_name/
# вернет ["first_name", "first_name="]Видно, что attr_accessor действительно создает экземпляр переменной @first_name и два метода в экземпляре (для получения и установки значений): first_name и first_name=
Реализация
Все классы наследуют методы класса от Module. Добавим методы-заглушки (mock methods):
class Module
# Сначала используем define_method
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
# Затем используем module_eval
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:0x28744 @name="John Smith", @phone="555-2344"h1>И
define_method, и module_eval дают одинаковый результат.Пример использования: ActiveRecord «для бедных»
Для тех, кто знаком с RubyonRails, будет легко понять реализацию чего-то, подобного на класс ActiveRecord, который просматривает имена полей в базе данных и добавляет к классу методы получения и задания значений.
Мы пойдем еще дальше и будем также динамически генерировать классы моделей.
В этом примере мы собираемся создать ActiveRecord «для бедных». Класс будет подключаться к базе MySQL, динамически генерировать класс для каждой найденной таблицы и заполнять классы методами установки и получения значений для полей таблицы.
require 'rubygems'
require 'mysql'
class PoorMan
# сохраним список сгенерированных классов в переменной экземпляра класса
class << self; attr_reader :generated_classes; end
@generated_classes = []
def initialize(attributes = nil)
if attributes
attributes.each_pair do |key, value|
instance_variable_set('@'+key, value)
end
end
end
def self.connect(host, user, password, database)
@@db = Mysql.new(host, user, password, database)
# пройдемся по списку таблиц базы данных и создадим классы для них
@@db.list_tables.each do |table_name|
class_name = table_name.split('_').collect { |word| word.capitalize }.join
# create new class for table with Module#const_set
@generated_classes << klass = Object.const_set(class_name, Class.new(PoorMan))
klass.module_eval do
@@fields = []
@@table_name = table_name
def fields; @@fields; end
end
# пройдемся по списку полей таблицы и создадим get/set-методы для них
@@db.list_fields(table_name).fetch_fields.each do |field|
# добавим get/set-методы
klass.send :attr_accessor, field.name
# добавим имена полей в список
klass.module_eval { @@fields << field.name }
end
end
end
# поиск строки по id
def self.find(id)
result = @@db.query("select * from #{@@table_name} where id = #{id} limit 1")
attributes = result.fetch_hash
new(attributes) if attributes
end
# поиск всех строк
def self.all
result = @@db.query("select * from #{@@table_name}")
found = []
while(attributes = result.fetch_hash) do
found << new(attributes)
end
found
end
end
# подключаем PoorMan к нашей базе данных, это всё, что осталось сделать
PoorMan::connect('host', 'user', 'password', 'database')
# печатаем список сгенерированных классов
p PoorMan::generated_classes
# ищем пользователя с id:1
user = Users.find(1)
# ищем всех пользователей
Users.allПереведено толпой. Основной переводчик — and_rew.
Текст опубликован на community.defun.ru, подготовлен в ХабраРедакторе



комментарии (19)