Сделать сложное простым: что такое DSL, или зачем вам новый язык программирования
Сделать простое иногда во много раз сложнее, чем сложное
© Михаил Калашников
Здравствуйте, меня зовут Владимир Кожаев, я фрилансер-разработчик инструментальных средств (звучит почти как анонимный алкоголик). Этой статьёй я открываю цикл материалов, посвящённый разработке парсеров, трансляторов, компиляторов и тому подобных инструментов, облегчающих жизнь.
Действительно, зачем это нужен ещё один язык программирования? Понятно, он может быть нужен в каких-то НИИ при университетах, но обыкновенному бизнесу — какой толк от этой заумной мути? Вообще к чему столько разных языков, почему бы не использовать один единственный? Давайте разберёмся.
Почему бы не пользоваться одним языком
Жил да был в Великобритании выдающийся математик, логик, криптограф, и звали его Алан Тьюринг. В числе других открытий он придумал машину имени себя. Опуская подробности, скажем, что с помощью этой машины можно реализовать всё то же, что и с помощью любых средств программирования более высокого уровня. То есть любую программу на любом языке можно переписать с помощью этого достаточно простого средства. Тем более на любом языке типа Java или PHP можно реализовать эту самую машину.
Как следствие, существует критерий полноты по Тьюрингу-Чёрчу. Язык называется полным, если на нём можно реализовать машину Тьюринга. Все популярные языки программирования общего назначения (Java, C#, PHP, Python, Scala, JavaScript и так далее) являются полными. Что же это означает? Все популярные языки эквивалентны! Ну вот, смотрите: мы знаем, что все программы можно выполнить с помощью машины. Машину же, которая выполняет, можно написать что на PHP, что на C++. Получается, одну и ту же программу, записав её на языке машины Тьюринга, можно выполнить везде. А мы знаем, что так можно записать вообще любую программу.
На практике это означает, что программу на языке, скажем, JavaScript, поднатужившись, можно переписать на С++. Обратное тоже справедливо. Да, имеют значение библиотеки и фреймворки, но саму логику можно без проблем перенести с одного языка на другой.
Зачем тогда разные языки нужны, почему бы не пользоваться одним? В знаменитом романе Семюэля Дилэни «Вавилон 17» описан человек с выключенной частью мозга. Вместо этого он обучен искусственному языку, близкому по синтаксису к записям математических выражений. Он замечательно подходит для быстрого решения логических задач, компактен и удобен, но ограничен. Например, отсутствовали слова «я» и «ты». Поэтому парадоксы, такие как «Севильский цирюльник», мозг ограниченный «Вавилоном 17», сожжет или заставит обратиться к отключенной части. То есть языковые конструкции во многом определяют способ мышления.
Рассмотрим язык математики более подробно. Вот, например, описание доказательства теорем методом математической индукции:
Расшифровывается это так. Допустим, что:
- Установлено, что P1 верно. (Это утверждение называется базой индукции)
- Для любого n доказано, что если верно Pn, то верно Pn+1. (Это утверждение называется индукционным переходом)
Тогда все утверждения нашей последовательности верны. Обратите внимание: вместо нескольких строк текста имеем лишь одну строку со строгим определением, понятным любому математику.
Что же такое DSL
Domain Specific Language, или язык предметной области, — это язык, созданный для конкретной области применения. Построение его, или структуры данных, отражают специфику решаемых им задач © Википедия.
То есть, если человек знает свою работу, учить DSL не надо — достаточно взглянуть один раз, и всё понятно (см. пример с математикой). Также хороший DSL не требует больших знаний в теории и практике программирования. Во многих, например, нет циклов. В некоторых — условных операторов (типа «if»). Часто язык является не полным по Тьюрингу, то есть написать любую программу с его помощью нельзя. Опять же, вспомним язык математики или кванторов. Он используется лишь для описания теорем или для их автоматического доказательства. Писать web-сервисы с его помощью было бы затруднительно.
Примеры использования DSL
DSL используются очень по-разному. Рассмотрим несколько из них и постараемся понять, в каком же случае следует их использовать.
Резак лазера
Положим, вы — инженер кораблестроитель и хотите вырезать большущую деталь для корпуса судна. Раньше это делалось так: на плотном картоне или фанере вычерчивали детальки, вырезали, прикладывали к листу стали, и люди, которых называли кернильщиками, ползали по листу и набивали по контуру выкройки впадинки. Дальше газорезчик шёл по контуру и вырезал. Представляете, что будет, если резчик с утра перебрал? А можно сделать это автоматически, чтобы робот считывал чертёж и сам ехал по листу, вырезая нужную деталь? Да, можно! Однако проблема в том, что траекторию его передвижения нужно как-то задать. Мол, поедь туда, опусти резак и дальше двигайся эдаким манером. Для этого нам нужны следующие команды:
- Двигаться из точки А в точку Б с выключенным резаком (помним, что прямая — кратчайшие расстояние между двумя точками).
- Двигаться с включенным резаком уже по заданной кривой (частный случай кривой — прямая). Для простоты ограничимся, собственно, отрезком прямой линии и участком окружности с заданным радиусом и центром. Поскольку положение резака задано предыдущими движениями, указать нужно лишь точку остановки. Для отрезка прямой — это конец отрезка. Для участка окружности — угол поворота и центр окружности.
Таким образом, в наиболее простом случае нам нужны только три команды:
MoveTo(x, y) LineTo(x,y) AngleTo(centerX,centerY, angle)
Как видите, язык очень простой, но с его помощью можно вырезать деталь любой сложности. Для программирования с его помощью достаточно навыков на уровне уверенного использования ПК. Добавив к этому языку переменные, условные операторы, циклы и процедуры, получим очень мощное средство. Как бонус — исследование кода методом белого ящика. К примеру, можно проверить, не вылезает ли наш резак за пределы листа металла.
Алгоритмический трейдинг
Трейдер редко ошибается дважды — обычно раза три или больше
© Из грустного опыта продавшего квартиру
Каждый хочет купить дешевле и продать дороже — вроде бы понятно. Но как определить правильное время для сделки, если завтра цена может вырасти или упасть? Решение принимается с помощью фундаментального (новости, анализ экономических и политических событий) и технического (экстраполяция стоимости ресурса на основании предыдущих данных). Признаки, по которым судят о поведении цены, теоретически не доказаны и не точны. То есть какая то связь с реальностью усматривается, но обычно берут несколько признаков и принимают решение о закрытии или открытии сделки, когда сигнал о покупке либо продаже подают все используемые индикаторы.
Кривая цены меняется очень быстро. Данные, полученные 15 минут назад, как правило, интересны только для историков. Деньги на бирже крутятся большие, так что потерять несколько сот миллионов долларов за минуту можно запросто. Поэтому человеческий фактор хорошо бы свести к минимуму. Но как это сделать, если общей теории поведения цены не существует и стратегии торговли трейдер выбирает их с помощью интуиции? Один из способов уберечься от ошибки — создать специальный язык с минимумом «шума». Оставить в языке только необходимое, безжалостно избавясь от возможностей, которые нам не нужны. Что же нужно для трейдинга?
- Понятно, что цена снега в Антарктиде и на экваторе, мягко говоря, отличается. То есть необходимо указать биржу, цены на которой мы исследуем.
- Нужно указать стратегию, с помощью которой мы будем торговать (их есть много разных).
- Для стратегии нужно указать параметры, специфические для каждой, и временной интервал, в течение которого происходит работа.
- Стратегии запускаются на серверах, каждый из которых работает с заданной биржей. Нужно задать время, в течение которого они работают, поскольку доступ к бирже бывает и платным.
Давайте посмотрим, как будет выглядеть эта стратегия на языке Java. Допустим, мы хотим получить сигналы о покупке/продаже валют на серверах трех бирж с помощью стратегий: «фибоначчи», «скользящее среднее», «преобразование Гильберта». Для простоты будем считать, что время измеряется в тиках, название биржи, на которой работает сервер, задается просто строкой, и торгуем мы валютами — меняем доллары, евро или ещё что-нибудь на украинскую гривню и обратно.
На первый взгляд, код выглядит хорошо, но, если приглядеться, в нём полно ошибок.
Во первых, время работы стратегий меньше, чем время, в течение которого работает сервер. Во вторых, мы запускаем только один сервер, вместо трёх. Так что с этим кодом материальные потери не заставят себя долго ждать.
С другой стороны, трейдинг — это постоянный стресс и гонка. Работать нужно действительно быстро, но без ошибок. Как же быть?
Давайте посмотрим, как мог бы выглядеть текст программы, представленной выше, записанный на действительно удобном языке.
Сверху маркер начала программы, дальше идёт список серверов. У каждого сервера один раз задается название биржи, с которой происходит работа, и время его работы. Дальше идёт список стратегий, каждая с специфическими параметрами.
Преимущества предложенного примера очевидны. Во первых, текст стал лаконичным: сервера указываются ровно один раз. Стратегии — в непосредственной близости от сервера, на котором запускаются. Во вторых, мы избавляемся от ненужных подробностей. Трейдеру вовсе и не нужно знать, что такое Thread или что итоговая программа будет написана на языке Java.
Разработка игровой логики
Ошибка: робот погибает при попадании в него гранаты (именно от попадания, а не от взрыва). Д — дизайнер, П — программист.
Д: программисты всё сломали! почему так получается?!
П: естественно, так получается! потому, что у гранаты масса 100 кг! зачем вы это сделали?
Д: да?! а чтобы граната в воде тонула!
П: а почему она с нормальной массой не тонет?
Д: а потому что у воды плотность большая! (прим.: больше, чем у ртути)
П: а почему плотность такая большая?!
Д: а чтобы ящики деревянные плавали!
П: а почему они иначе не плавают?!
Д: а потому что у них масса 50 кг!
П: а зачем такая масса?!
Д: а иначе они некрасиво разваливаются!
Допустим, вы — геймдизайнер, и вам нужно создать сценарий для поведения робота. В начале стрелять, когда кончатся патроны — бежать. Программисты это, конечно, без проблем сделают. Но в определённый момент нужно изменить поведение — сделать так, чтобы робот в начале бежал и только когда догоняют — стрелял. А ещё — прятался за холмик или делал забавный финт ушами. Программист, конечно, снова сделает, но потратит время. При последующих переделках придётся опять его тревожить, и так без конца.
Более того, игры сейчас выходят на множестве разных платформ. Выпустили под Windows, и надо выходить на Vii, на планшетах, на smart TV и так далее. Каждый релиз приводит к переписыванию кода, который уже работает и оттестирован, хотя логика действий персонажей не меняется при переходе от устройству к устройству. Можно, конечно, использовать кроссплатформенные средства. Такие как Unity, или Haxe, но, как правило, проблема в том, что кроссплатформа работает одинаково плохо на всех устройствах. То есть хотелось бы сделать так, чтобы разрабатывать заново нужно было только специфические для конкретной платформы вещи, оставив логику без изменений.
Можно ещё использовать для логики скриптовые языки, однако даже они слишком сложны для того, чтобы использовать их без изучения. Там много подробностей, нужных для программиста, но лишних для конструкций: «Если произошло это — сделай то».
Что же делать, учить дизайнера программированию? Но это две довольно разные и в каком-то смысле противоположные специальности. Хотелось бы сделать так, чтобы дизайнер достаточно простым способом без помощи программиста мог поменять поведение персонажей.
Конечный автомат
Представим игровую логику в виде состояний персонажа и переходов между ними. К примеру, у робота может быть три состояния: «бежать к игроку», «стрелять» и «искать патроны», когда они кончились. Действия происходят при входе в состояние, выходе из него, переходе от одного состояния в другое и когда состояние между через определенный метод времени не изменилось. Можно описать состояния и переходы с помощью JSON, или XML и потом воспользоваться шаблоном проектирования «машина состояний», как это описано в банде четырёх. XML для описания представлен ниже:
<state name="run_to_enemy"> <before methods="do_something_before"/> <after methods="say_hi"/> <in_process methods="say_hug"/> <transitions> <transition name="shoot" methods="run"> <condition function="near_the_enemy && have_bullets"/> </transition> </transitions> </state> <state name="shoot"> <before methods="do_something_before_shoot"/> <after methods="say_hia"/> <in_process methods="say_bum"/> <transitions> <transition name="run_to_bullets" methods="hi"> <condition function="no_bullets"/> </transition> </transitions> </state> <state name="run_to_bullets"> <before methods=""/> <after methods=""/> <in_process methods="run"/> <transitions> <transition name="run_to_enemy" methods="eat_bullets"> <condition function="near_the_bullets"/> </transition> </transitions> </state>
Но XML очень не удобен для программирования. Покажем, как это описать с помощью DSL-языка.
Как видите, описание стало гораздо более лаконичным и удобочитаемым. Появилась подсветка синтаксиса. Скажу вам по секрету, автокомплит и подсветка ошибок тоже есть.
Таким образом, можно отделять игровую логику от платформозависимых вещей: графики, ввода-вывода, управления и даже от того, как методы «стрелять», «бежать» и «кричать» реализованы на практике. Последнее является частным случаем декларативного программирования: вместо того, чтобы реализовывать детальный алгоритм, мы описываем конечный результат. Вместо того, чтобы говорить компьютеру как делать, мы говорим что.
Выводы
Все рассмотренные DSL:
- Небольшие и не требуют изучения. Это справедливо и в общем: язык предметной области с большим порогом входа — плохой.
- Позволяют оперировать терминами предметной области, без деталей программной реализации. Говорят ЧТО делать, а не КАК.
- Избавляют специалиста от необходимости получать высокую квалификацию в программировании.
DSL применяется, когда необходимо записать достаточно сложную логику и избавить специалиста в определённом домене от необходимости изучать программирование, а программиста — разбираться в предмете. Обратно, если для реализации задачи не нужно обладать квалификацией помимо собственно программирования, DSL вам не нужен.
Вторая статья будет посвящена графическим языкам программирования, последующие — способам реализовать DSL и применениям их в разных, иногда неожиданных областях.