Pull to refresh

DSL и динамические вкусности Ruby

Ruby *
В этой статье я проиллюстрирую основные возможности Ruby для построения Domain Specific Languages(DSL). DSL, это небольшие, узкоспециализированные языки для решения конкретных задач. В отличие от языков общего назначения, таких как C++ или Java, DSL обычно очень компактны, и обладают высокой выразительностью в контексте решаемой задачи.

Различные DSL широко распространены в библиотеках и фреймворках для Ruby. Например в Rails DSL используются для создания миграций.

А теперь, давайте посмотрим какие возможности Ruby предоставляет для построения DSL


Пусть нам нужен простой формат для описания комплектации компьютера.
Простой пример:
Процессор: 2.2 гигагерц
Память: 1 гигабайт
Диск: 250 гигабайт

Теперь с помощью Ruby построим удобный DSL для таких описаний.

Этап 1.


Трансформируем даное описание в Ruby код, например так(пусть память мы храним в мегабайтах а частоту в мегагерцах)
comp = Computer.new
comp.cpu = 2.2 * 1024
comp.ram = 2 * 1024
comp.disk = 1 * 1024

Код класса элементарный:
class Computer
    attr_accessor :cpu
    attr_accessor :ram
    attr_accessor :disk
end

В Ruby все переменные экземпляра(такие переменные начинаются с @) являются приватными, т.е. доступны только внутри методов объекта. Чтобы сделать аттрибут, мы должны объявить два метода для установки и получения значения этого аттрибута:
def cpu
    @cpu
end

def cpu= val
    @cpu = val
end

или проще, вызвать: attr_accessor :cpu, который и сгенерирует нам эти методы

Этап 2.


Ну и что? ничего нового тут нет, просто используем объект с аттрибутами. Попробуем немного усовершенствовать наш код.
Первое что бросается в глаза, мы должны самостоятельно переводить гигабайты в мегабайты и тд. Исправим!
comp = Computer.new
comp.cpu = 2.2.ghz
comp.ram = 2.gb
comp.disk = 1.gb

Для этого примешаем методы ghz и gb к классу Numeric
class Numeric
    def ghz
        self*1000
    end
    def gb
        self*1024
    end
    def mhz
        self
    end
    def mb
        self
    end
end

Еще я добавил два метода mhz и mb, cpu.ram = 512.mb вместо cpu.ram = 512.

В Ruby есть возможность примешать(mixin) новый метод к любому классу. Т.е. мы можем расширять класс даже после его создания. После того как мы примешали метод к классу, он становится доступным для всех его экземпляров.
class String
    def cool
        self + " is cool!"
    end
end

В методе cool self — это указатель на значения самого объекта, а так как возвращаемое значение метода это результат выполнения его последней строки, то
puts "my string".cool
выведет на экран «My string is cool!»

Методы ghz и тд я примешал к классу Numeric, потому что это родительский класс для всех чисел в Ruby. И для целых и для дробных

Этап 3.


Уже лучше. Но еще смущает тот факт, что перед каждым параметром мы должны указывать «comp.». Сделаем немного подругому:
comp = Computer.new do
    cpu 2.2.ghz
    ram 2.gb
    disk 1.gb
end

Выглядит намного лучше, не правда ли? Но вопрос будет ли это работать?
Давайте разберемся. На вид, это валидный Ruby код. cpu, ram и disk это уже не методы, а функции, так как вызываются не у экземпляра класса Computer.
Что то наподобии этого:
def cpu val
    comp.cpu = val
end

Но как нам передать в эту функцию переменную comp?
cpu 2.ghz, comp? но тогда потяряется вся выразительность. Вот если бы мы могли выполнить эти методы в контексте этого объекта…
И ведь мы можем! Ruby дает нам такую возможность c помощью метода instance_eval.

Теперь посмотрим на новую реализацию класса Computer
class Computer
    #метод initialize это конструктор
    def initialize &block #&block означает что методу передается блок кода
        instance_eval &block #вызываем волшебный метод instance_eval и передаем ему блок
    end

    #тут я объявил методы вместо аттрибутов, чтобы вместо cpu= 2.ghz писать cpu 2.ghz
    def cpu val
        @cpu_clock = val
    end

    def ram val
        @ram_size = val
    end

    def disk val
        @disk_size = val
    end

    #в установке значения теперь нет нужды, так как есть методы cpu и тд
    #по этому я вызываю attr вместо attr_accessor, он не будет генерировать метод для установки значения
    #но есть небольшое ограничение, т.к. в динамических языках нельзя перегружать методы
    #(a attr_accessor по настоящему создает методы)
    #то для аттрибутов нужно выбрать другие имена
    attr :cpu_clock
    attr :ram_size
    attr :disk_size
end


Что же делает этот метод? Все очень просто. Как уже сказал выше, он выполняет блок(а можно и строку с кодом) в контексте данного объекта.
А так как в классе Computer объявлены методы cpu и тд, то они и вызовутся. И именно для этого объекта.

Этап 4.


Теперь представим, что у нашего компьютера неограниченно много различных характеристик. Например, размер BIOS'a или разрадность шины:
comp = Computer.new do
    bios 0.5.mb
    bus 100
end


Все мы предугадать не можем, но хочется чтобы была возможность добавления таких характеристик.
И тут нам снова приходят на помощь возможности Ruby, а именно метод method_missing.
method_missing специальный метод объекта, который вызывается при попытке вызвать несуществующий метод. Пример:
class Test
    def method_missing name, *args, &block #name - имя метода, *args - его аттрибуты, &block - блок кода если есть.
        puts name.to_s + " called"
    end
end

t = Test.new
t.some_method #напечатает "some_method called"
t.asdf #"asdf called"


Теперь вернемся к классу Computer:
class Computer
    def initialize &block
        instance_eval &block
    end
    
    def method_missing name, *args, &block
        instance_variable_set("@#{name}".to_sym, args[0]) #создаем переменную экземпляра и присваеваем ей значение
        self.class.send(:define_method, name, proc { instance_variable_get("@#{name}")}) #создаем метод для доступа к этой переменной
    end
end


Теперь как это работает. Мы передаем блок конструктору нашего класса. Этот блок выполняется в контексте экземпляра этого класса. В процессе выполнения вызываются несуществующие методы, вместо них выполняется method_missing с параметром name равным имени метода и массивом аргументов args. Теперь мы создаем переменную экземпляра с именем, совпадающим с методом, и значением, равным первому аттрибуту вызванного метода. А также метод для получения значения этой переменной.

Кстати, method_missing используется в Rails в ActiveRecord, для создание тысяч методов типо Person.find_all_by_name

Получившийся код можно посмотреть тут.

Дальнейшие усовершенствования


Что еще можно придумать, чтобы наш DSL стал еще удобнее?
Так как DSL могут быть предназначены не только для программистов, но и для людей незнакомых с программированием, то логично вынести описания в файл, который может редактировать даже не программист. А потом загружать и интерпретировать этот файл.
my_pc.conf:
cpu 1.8.mgh
ram 512.mb
disk 40.gb

Сделать это очень просто, как я уже писал выше, методу instance_eval можно передать строку с кодом вместо блока.

Во-вторых, для фанатов русского языка, можно писать так:
comp = Computer.new do
процессор 2.2.ghz
память 2.gb
диск 1.gb
end

Чтобы это сработало, надо просто добавить параметр -Ku при вызове интерпретатора. Например так:
ruby -Ku test.rb

Выше мы построили простой DSL, который является валидным Ruby кодом. Но можно отказаться от валидности. Например избавиться от точки.
Вместо cpu 1.ghz писать cpu 1ghz. Тогда придется произвести небольшой препроцессинг. Добавить эти точки, например с помощью регулярных выражений.

А теперь, если мы скомбинируем эти улучшения, то мы сможем проинтерпритировать пример, который я дал в самом начале:
Процессор: 2.2 гигагерц
Память: 1 гигабайт
Диск: 250 гигабайт

Попробуйте сами:)

Теперь немножко саморекламы:)
Когда я сам разбирался с этой методикой, я написал простую библиотеку с DSL для генерации валидного XHTML.
Смысл в том, что если мы пытаемся написать чтото невалидное, то получаем ошибку прямо в процессе генерации.
Я оформил ее как gem пакет, так что можно поставить так:
в Windows: gem install rml
в *nix системах: sudo gem install rml
Страница проекта: http://rubyforge.org/projects/rml/.
Примеры и документацию можно посмотреть: http://rml.rubyforge.org/.
Правда там на английском, но если заинтересовало, могу написать небольшую заметку.
Tags:
Hubs:
Total votes 48: ↑44 and ↓4 +40
Views 11K
Comments 43
Comments Comments 43

Posts