Что такое метапрограммирование?
Metaprogramming is the writing of computer programs that write or manipulate other programs (or themselves) as their data, or that do part of the work at compile time that would otherwise be done at runtime. In many cases, this allows programmers to get more done in the same amount of time as they would take to write all the code manually, or it gives programs greater flexibility to efficiently handle new situations without recompilation. (via Wikipedia)
Это руководство перечисляет все стандартные методы Ruby, используемые для метапрограммирования, а так же приводит примеры их обычного употребления. В конце мы получим пример использования метапрограммирования для динамического создания классов, соответствующих таблицам в БД, включающих методы, соответствующие полям таблицы, на манер ActiveRecord
.
Инструментарий метапрограммиста
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
Получение, установка и удаление констант (и классов)
Объявление и удаление методов
Выполнение кода, построенного «на лету»
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. Когда нужно выполнить код внутри контекста какого-то объекта, можно использовать методы instance_eval
и module_eval
(синоним class_eval
).
Метод instance_eval
выполняет код в контексте существующего объекта.
[1, 2, 3, 4].instance_eval('size') # вернёт 4
В примере мы передали методу instance_eval
строку 'size'
, что интерпретировалось как получение метода :size
массивом. Это эквивалентно следующему вызову:
[1, 2, 3, 4].size
Кроме того, вы можете передавать методу instance_eval
блок.
# Получаем среднее арифметическое чисел в массиве
[1, 2, 3, 4].instance_eval { inject(:+) / size.to_f } # вернёт 2.5
Методы 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
['ничего', 'один', 'пара', 'несколько'][self]
elsif self > 3
'много'
elsif self < 0
'отрицательно'
end
end
end
1.to_word # вернёт 'один'
2.to_word # вернёт 'пара'
Мы видим, как 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
Пример выше создаёт метод класса (или «метод-синглетон») create_multiplier
, с помощью которого мы сможем создавать методы, применимые к любому объекту класса 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) # returns [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
динамически генерирует getter и setter для переменной экземпляра. Давайте посмотрим на примере.
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
и два метода: getter first_name
и setter first_name=
.
Реализация
Все классы наследуют методы Module
, поэтому мы поместим методы сюда.
class Module
# First using 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
# Second using 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 # returns
define_method
и module_eval
оба вернут один и тот же результат.
Пример использования: Active Record для бедных
Те, кто знаком с Ruby on Rails, уже догадываются, как можно реализовать класс ActiveRecord
, который пройдётся по полям таблицы и добавит классу соответствующие методы getter и setter для полей таблицы.
Мы можем пойти дальше и динамически создать классы, соответствующие таблицам.
В этом примере мы создадим ActiveRecord
для бедных. Этот класс соединится с базой данных MySQL, создаст динамически классы, соответствующие каждой таблице, а так же наполнит эти классы методами getter и setter, соответсвующими полям таблицы.
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
# создаём класс для таблицы, используя 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
# пройдёмся по списку полей таблицы и создадим методы getter и setter для них
@@db.list_fields(table_name).fetch_fields.each do |field|
# добавляем getter и setter
klass.send :attr_accessor, field.name
# добавляем имя поля в список полей
klass.module_eval { @@fields << field.name }
end
end
end
# получаем строку таблицы по идентификатору
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
# получаем пользователя с идентификатором 1
user = Users.find(1)
# получаем всех пользователей
Users.all
Оригинал статьи: Ruby’s metaprogramming toolbox, автор — Corban Brook
Некоторые примеры могут не работать на Ruby < 1.8.7