В этой статье я проиллюстрирую основные возможности Ruby для построения Domain Specific Languages(DSL). DSL, это небольшие, узкоспециализированные языки для решения конкретных задач. В отличие от языков общего назначения, таких как C++ или Java, DSL обычно очень компактны, и обладают высокой выразительностью в контексте решаемой задачи.
Различные DSL широко распространены в библиотеках и фреймворках для Ruby. Например в Rails DSL используются для создания миграций.
А теперь, давайте посмотрим какие возможности Ruby предоставляет для построения DSL
Пусть нам нужен простой формат для описания комплектации компьютера.
Простой пример:
Теперь с помощью Ruby построим удобный DSL для таких описаний.
Трансформируем даное описание в Ruby код, например так(пусть память мы храним в мегабайтах а частоту в мегагерцах)
Код класса элементарный:
В Ruby все переменные экземпляра(такие переменные начинаются с @) являются приватными, т.е. доступны только внутри методов объекта. Чтобы сделать аттрибут, мы должны объявить два метода для установки и получения значения этого аттрибута:
или проще, вызвать:
Ну и что? ничего нового тут нет, просто используем объект с аттрибутами. Попробуем немного усовершенствовать наш код.
Первое что бросается в глаза, мы должны самостоятельно переводить гигабайты в мегабайты и тд. Исправим!
Для этого примешаем методы
Еще я добавил два метода mhz и mb, cpu.ram = 512.mb вместо cpu.ram = 512.
В Ruby есть возможность примешать(mixin) новый метод к любому классу. Т.е. мы можем расширять класс даже после его создания. После того как мы примешали метод к классу, он становится доступным для всех его экземпляров.
В методе
выведет на экран «My string is cool!»
Методы ghz и тд я примешал к классу Numeric, потому что это родительский класс для всех чисел в Ruby. И для целых и для дробных
Уже лучше. Но еще смущает тот факт, что перед каждым параметром мы должны указывать «comp.». Сделаем немного подругому:
Выглядит намного лучше, не правда ли? Но вопрос будет ли это работать?
Давайте разберемся. На вид, это валидный Ruby код. cpu, ram и disk это уже не методы, а функции, так как вызываются не у экземпляра класса Computer.
Что то наподобии этого:
Но как нам передать в эту функцию переменную comp?
cpu 2.ghz, comp? но тогда потяряется вся выразительность. Вот если бы мы могли выполнить эти методы в контексте этого объекта…
И ведь мы можем! Ruby дает нам такую возможность c помощью метода instance_eval.
Теперь посмотрим на новую реализацию класса Computer
Что же делает этот метод? Все очень просто. Как уже сказал выше, он выполняет блок(а можно и строку с кодом) в контексте данного объекта.
А так как в классе Computer объявлены методы cpu и тд, то они и вызовутся. И именно для этого объекта.
Теперь представим, что у нашего компьютера неограниченно много различных характеристик. Например, размер BIOS'a или разрадность шины:
Все мы предугадать не можем, но хочется чтобы была возможность добавления таких характеристик.
И тут нам снова приходят на помощь возможности Ruby, а именно метод method_missing.
method_missing специальный метод объекта, который вызывается при попытке вызвать несуществующий метод. Пример:
Теперь вернемся к классу Computer:
Теперь как это работает. Мы передаем блок конструктору нашего класса. Этот блок выполняется в контексте экземпляра этого класса. В процессе выполнения вызываются несуществующие методы, вместо них выполняется
Кстати, method_missing используется в Rails в ActiveRecord, для создание тысяч методов типо
Получившийся код можно посмотреть тут.
Что еще можно придумать, чтобы наш DSL стал еще удобнее?
Так как DSL могут быть предназначены не только для программистов, но и для людей незнакомых с программированием, то логично вынести описания в файл, который может редактировать даже не программист. А потом загружать и интерпретировать этот файл.
my_pc.conf:
Сделать это очень просто, как я уже писал выше, методу instance_eval можно передать строку с кодом вместо блока.
Во-вторых, для фанатов русского языка, можно писать так:
Чтобы это сработало, надо просто добавить параметр -Ku при вызове интерпретатора. Например так:
Выше мы построили простой DSL, который является валидным Ruby кодом. Но можно отказаться от валидности. Например избавиться от точки.
Вместо
А теперь, если мы скомбинируем эти улучшения, то мы сможем проинтерпритировать пример, который я дал в самом начале:
Попробуйте сами:)
Теперь немножко саморекламы:)
Когда я сам разбирался с этой методикой, я написал простую библиотеку с DSL для генерации валидного XHTML.
Смысл в том, что если мы пытаемся написать чтото невалидное, то получаем ошибку прямо в процессе генерации.
Я оформил ее как gem пакет, так что можно поставить так:
в Windows:
в *nix системах:
Страница проекта: http://rubyforge.org/projects/rml/.
Примеры и документацию можно посмотреть: http://rml.rubyforge.org/.
Правда там на английском, но если заинтересовало, могу написать небольшую заметку.
Различные 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/.
Правда там на английском, но если заинтересовало, могу написать небольшую заметку.