Ruby

индекс
128,98

Инструменты метапрограммирования в Ruby

Что такое «метапрограммирование»?


Метапрограммирование — это вид программирования, связанный с созданием программ, которые порождают другие программы как результат своей работы (в частности, на стадии компиляции их исходного кода), либо программ, которые меняют себя во время выполнения (самомодифицирующийся код). Первое позволяет получать программы при меньших затратах времени и усилий на кодирование, чем если бы программист писал их вручную целиком, второе позволяет улучшить свойства кода (размер и быстродействие) (из 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, подготовлен в ХабраРедакторе
+28
8 октября 2009, 18:38
51

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

+2
imps #
красивую картинку рубина нашли
0
Freek #
довольно неплохо, а в других языках такая реализация возможна?
0
thevery #
java/groovy — проще простого
0
sha1dy #
ну чтоб прямо вот так через define_method в java — ну не говорите глупостей
0
folone #
Вы про груви слыхали?
0
thevery #
«чтобы вот так» можно в groovy, я ж не зря про него написал.
0
Talleyran #
ну и что делать с этим вашим груви… я например ком мост хочу, есть такое? ява уродлива.
0
folone #
sourceforge.net/projects/jacob-project/
для груви — groovy.codehaus.org/COM+scripting
ваше заявление безосновательно.
0
thevery #
если вы про COM — да, легко:
net = new ActiveXProxy('Wscript.Network')
println «Name of this computer: ${net.ComputerName.value}»
0
Talleyran #
#ruby
require 'win32ole'
v = WIN32OLE.new 'Wscript.Network'
p «Name of this computer: #{v.ComputerName}» #-.value-

#python
import win32com.client
v = win32com.client.Dispatch('Wscript.Network')
print «Name of this computer: {0}».format(v.ComputerName) #-.value-

Эта либа по меньшей мере не интуитивна.
0
thevery #
ээ… а в каком именно месте ваш код более интуитивно понятен, чем мой? или вы считаете, что printf — это лучше, чем print? (а других-то различий вообще нету)
0
Talleyran #
# — знак комментария в обоих языках, это я хотел показать зачеркнутое value, но эти теги не зачеркивают нихера почему-то…
еще раз
#ruby
require 'win32ole'
v = WIN32OLE.new 'Wscript.Network'
p «Name of this computer: #{ v.ComputerName }»

#python
import win32com.client
v = win32com.client.Dispatch('Wscript.Network')
print «Name of this computer: {0}».format( v.ComputerName )

методы вызываются так же как в VBscript, а в VBscript нет value, и я полагаю это только начало, т.к. ява ком мост я смотрел, когда наивно полагал, что удасться нормально работать с com из jruby.
Нет _ниодной_ вменяемой причины так сложно организовывать доступ к простому интерфейсу.
0
thevery #
а, вот оно что…
.value в данном случае не обязательно
+1
aratak #
Надеюсь, ваша статья привлечет еще руби разработчиков ;)
Спасибо.
НЛО прилетело и опубликовало эту надпись здесь
0
simonoff #
Спасибо за статью!!!
+2
preprocessor #
И ни слова про биндинги. А именно про них далеко не все знают.
0
nduce #
Если есть оригинал про биндинги, можно предложить на translated.by.
0
akhkharu #
Отличная статья, спасибо!

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