В этой статье я проиллюстрирую основные возможности 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/.
Правда там на английском, но если заинтересовало, могу написать небольшую заметку.
