Статьи

Посмотрите на DSL

Если вы до сих пор не видели и не писали XML, считайте себя очень счастливым.

XML начинался как очень дружественный и разумный способ представления данных, чтобы они были удобочитаемыми и машиночитаемыми одновременно. Конечно, это звучало чертовски здорово, пока люди не начали создавать невероятно большие системы (и, возможно, сверхинженерные системы) на основе XML. Этот сайт является приятным местом для программистов всех уровней, поэтому мы не будем упоминать какие-либо конкретные технологии … но, вы знаете, кто вы есть;)

XML не так уж и ужасен — он имеет большой смысл для некоторых приложений. Но вы не можете обойти тот факт, что XML иногда раздражает, и некоторые приложения действительно нуждаются в большей гибкости, чем XML.

DSL (предметно-ориентированный язык) — это небольшой, специализированный язык, который предназначен для выполнения нескольких задач, но делает их хорошо. Примером может служить Gemfile «DSL», который поставляется с Rails. Фактически, отсутствие XML было одной из самых больших приманок в Rails (для меня и, скорее всего, для многих других).

Дело в том, что XML — это только данные. Это не язык программирования; Таким образом, вы не можете выразить какие-либо вычисления с ним. И это не самый читаемый во многих случаях.

Веб-фреймворки — это не единственное место, где возникает ситуация, когда XML (или любой другой подобный формат обработки данных) не имеет особого смысла. Vim и Emacs имеют специфичные для домена языки. Как правило, к финансовым приложениям обычно прикрепляются какие-то DSL. Дело в том, что DSL обладает мощью, которую не может предложить ни один формат представления данных.

Это не значит, что DSL нельзя использовать для представления данных! Они уверены, что могут, и, на самом деле, при правильной разработке они работают лучше, чем многие другие традиционные системы, такие как XML или JSON.

В прежние времена DSL было не так легко написать. Вы должны были определить язык, используя формальный метод, сгенерировать лексер и парсер и, наконец, скомпилировать или интерпретировать язык. Это было действительно много работы, потому что вы написали WRITE A FREAKING COMPILER.

Но с динамическими языками жизнь намного проще, а DSL действительно набирает популярность. Основная идея, вместо того, чтобы писать
компилятор, мы используем возможности самого Ruby, чтобы помочь в разработке языка. Мы определили несколько методов, и, используя эти методы, пользователь DSL может достичь своей цели, используя Ruby. Этот вид программирования часто называют « метапрограммированием », поскольку он манипулирует другим кодом.

Особая идея заключается в том, что если вы действительно не хотите этого, вы полностью скрыты от Ruby. Это не очень похоже на Ruby:

forward 1
left 16
forward 5
right 6
backward 3

view raw
gistfile1.rb
hosted with ❤ by GitHub

Но это очень простой DSL, который мы можем использовать для определения Ruby.

До сих пор мы только что говорили об этом. Давайте продолжим создавать DSL с Ruby!

Определение

Прежде всего, нам нужно понять, что именно мы хотим от языка и как должен выглядеть DSL.

Давайте представим робота; он может двигаться только вперед, назад, вправо и влево на некоторое количество единиц. Мы должны отслеживать его координаты по отношению к точке, с которой он начал, которую мы будем называть началом координат. Он также может стрелять, и мы должны отслеживать координаты, по которым стреляет робот.

Это довольно четкое описание. Это может быть что-то, что используется для веб-сайта, на котором проводятся соревнования для таких роботов (загрузите ваш код, и ваш робот будет противостоять другим). Было бы весело, если бы в DSL было еще несколько функций! Но мы пойдем с KISS сейчас.

Теперь нам нужно иметь пример кода, чтобы пойти с этим:

forward 1
fire
backward 2
fire
right 16
fire
backward 2
fire
left 2

view raw
gistfile1.txt
hosted with ❤ by GitHub

В порядке, что это хорошо. Это ясно показывает, как мы хотим, чтобы наш язык выглядел. Мы просто должны это реализовать.

Реализация

Реализация DSL невероятно проста, на самом деле настолько проста, что вам захочется делать это постоянно!

Мы загружаем немного кода из файла и используем специальный маленький метод с именем «instance_eval», чтобы запустить его как код Ruby, но с некоторыми специальными методами. В нашем DSL мы хотим определить методы «вперед», «назад», «вправо», «влево» и «огонь».

Для этого мы определяем класс (вы можете называть его как угодно, я буду называть его роботом):

class Robot
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Добавьте в методы:

class Robot
def forward
end
def backward
end
def right
end
def left
end
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Теперь, в качестве практической демонстрации, мы добавляем код, который выводит имя метода с именем:

Теперь мы используем instance_eval. Его можно использовать с любым (в значительной степени) любым объектом, и он принимает блок в качестве аргумента. Вот:

class Robot
def forward
puts «forward»
end
def backward
puts «backward»
end
def right
puts «right»
end
def left
puts «left»
end
end
Robot.new.instance_eval do
forward
backward
left
right
right
end

view raw
gistfile1.txt
hosted with ❤ by GitHub

Это должно распечатать порядок методов при запуске. Почему это круто? Потому что это показывает нам, что небо это предел!

Поскольку мы можем определить, какого черта мы хотим в классе Robot, создание DSL становится очень простым и требует большого творческого потенциала.

Помните, мы хотим отслеживать координаты робота и координаты, по которым мы стреляли. Давайте сначала займемся первым.

Мы начинаем с добавления в конструктор (и избавляемся от «пут» в методах, которые были только в качестве примера):

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward
end
def backward
end
def right
end
def left
end
end

view raw
gistfile1.txt
hosted with ❤ by GitHub

Мы добавили необходимые структуры данных в качестве переменных объекта, чтобы отслеживать информацию о нашем любимом роботе.

Теперь мы заполним методы перемещения, чтобы они фактически изменили информацию, которую мы отслеживаем:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
end
def backward(n)
@coordinates[0] -= n
end
def right(n)
@coordinates[1] += n
end
def left(n)
@coordinates[1] -= n
end
end

view raw
gistfile1.txt
hosted with ❤ by GitHub

Мы добавляем метод «огонь» и логику, необходимую для хранения местоположений fired_at:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
end
def backward(n)
@coordinates[0] -= n
end
def right(n)
@coordinates[1] += n
end
def left(n)
@coordinates[1] -= n
end
def fire
@fired_at.push(@coordinates)
end
end

view raw
gistfile1.txt
hosted with ❤ by GitHub

Перед тестированием мы должны добавить некоторые «путы», чтобы мы могли реально увидеть, что происходит:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
puts @coordinates
end
def backward(n)
@coordinates[0] -= n
puts @coordinates
end
def right(n)
@coordinates[1] += n
puts @coordinates
end
def left(n)
@coordinates[1] -= n
puts @coordinates
end
def fire
@fired_at.push(@coordinates)
puts @coordinates
end
end

view raw
gistfile1.txt
hosted with ❤ by GitHub

Наконец, мы добавляем instance_eval обратно с небольшим количеством тестового кода, чтобы увидеть, находится ли то, что мы разработали, на правильном пути:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
puts @coordinates, «——«
end
def backward(n)
@coordinates[0] -= n
puts @coordinates, «——«
end
def right(n)
@coordinates[1] += n
puts @coordinates, «——«
end
def left(n)
@coordinates[1] -= n
puts @coordinates, «——«
end
def fire
@fired_at.push(@coordinates)
puts @coordinates
puts @fired_at, «——«
end
end
Robot.new.instance_eval do
backward 1
forward 2
left 6
fire
right 5
fire
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

При этом вы должны получить выходные данные координат и fired_at’s робота.

Вот так легко разработать DSL! Напомним, что это все, что мы сделали:

  • Сделал класс
  • Добавлены методы (необязательно, хотя без них DSL на самом деле ничего не значит)
  • Добавлен конструктор (необязательно)
  • Используется instance_eval

Это все, что нужно сделать! Неудивительно, что DSL настолько популярны — они дают нам много энергии для очень небольшого кода.

Помните, что у вас все еще есть Руби. Рассмотрим этот фрагмент кода:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
puts @coordinates, «——«
end
def backward(n)
@coordinates[0] -= n
puts @coordinates, «——«
end
def right(n)
@coordinates[1] += n
puts @coordinates, «——«
end
def left(n)
@coordinates[1] -= n
puts @coordinates, «——«
end
def fire
@fired_at.push(@coordinates)
puts @coordinates
puts @fired_at, «——«
end
end
Robot.new.instance_eval do
def do_nothing
forward 1
backward 1
end
do_nothing
right 6
end

view raw
gistfile1.rb
hosted with ❤ by GitHub

Если вы запустите его, то увидите, что робот заканчивается (0, 6). Мы использовали Ruby для создания некоторой абстракции для робота, чтобы мы могли создавать большие программы!

подсказки

При разработке DSL нужно помнить несколько вещей:

  • Держите очень простой API — короткие имена, простые идеи
  • Помните, что Ruby на вашей стороне — используйте его в полной мере вместе с вашим DSL
  • DSL расшифровывается как Domain Specific Language — вы не пытаетесь изобрести здесь другой Ruby, так что держите его маленьким
  • Если DSL будет введен пользователем на сервер — никогда не доверяйте своим пользователям

Последний пункт заслуживает более подробного объяснения. Если вы используете Robot DSL для сопоставления пользовательских роботов друг с другом, в условиях, когда кто-то загружает свой код на ваш сервер, а затем ваш сервер запускает код, будьте предельно осторожны в отношении того, что могут сделать ваши пользователи!

DSL НЕ защищают вас от того, что может сделать Ruby — весь код должен быть изолирован, и (в основном) весь доступ к syscall должен быть отключен. Если внешний код будет запущен на ваших серверах, вы будете взломаны, прежде чем сможете сказать «язык, специфичный для домена»!

Вывод

Я надеюсь, что вы получили удовольствие от поездки; мы разработали язык, специфичный для предметной области, примерно в 50 строках кода Это довольно круто!

Небо действительно предел с DSL! Их так легко построить, а еще проще сделать их лучше! Просто добавьте или измените методы в классе, для которого вызывается instance_eval.

Спасибо за чтение, и я надеюсь, что знания пригодятся.