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

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

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

Представим, как бы мог выглядеть этот конфигурационный файл в формате XML. Пусть конкретный светофор ведет себя следующим образом. 10 секунд горит красный, затем 3 секунды мигает желтый и, наконец, 7 секунд горит зеленый. Проявив немного фантазии, можно представить себе вот такой XML-файл:

<trafficlight>
  <red>10</red>
  <yellow blink="yes">3</yellow>
  <green>7</green>
</trafficlight>

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

Каковы минусы? В этой конфигурации большое количество визуального мусора и ненужных деталей. Этот «код» плохо читаем. Читаемость очень важна при общении с экспертами предметной области и в их дальнейшей самостоятельной работе. Но мы обычно ленимся предложить что-то удобнее XML, а заказчики зачастую даже не знают, что можно делать иначе.

Но иначе можно.

Что, если записать настройки так?

red 10
yellow 3 blink
green 7

Уже чуть больше ясности. Или даже так, добавим гибкости, чтобы в будущем иметь возможность менять начальное состояние светофора и задавать более сложные режимы работы:

init
  red off
  yellow off
  green off
endinit
loop
  red on
  wait 10
  red off
  yellow on
  wait 1
  yellow off
  wait 1
  yellow on
  wait 1
  yellow off
  green on
  wait 7
  green off
endloop

Мне нравится эта программа. Давайте попробуем её выполнить. Так как у меня дома нет настоящего светофора (а жаль), то программировать придется его простую программную модель.

Скриншот программы TrafficLight

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

Для автоматизации задач синтаксического и лексического анализа текста часто используют утилиты вроде Lex/Yacc. Недавно я писал о попытке реанимировать их версию для Delphi. И здесь они к месту.

Не буду пытаться объять необъятное, на просторах интернета множество руководств по использованию Lex и Yacc. Буду краток. С помощью данных утилит можно сгенерировать код парсера некоего языка по его формальному описанию. Собственно, они позволяют сгенерировать два куска кода: лексер (Lex) и парсер (Yacc). Лексер будет разбивать текст на токены, а парсер собирать эти токены в большую осмысленную программу.

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

Описание токенов в нашем случае весьма однообразное. В языке программирования светофора есть ключевые слова RED, YELLOW, GREEN, ON, OFF, INIT, ENDINIT, LOOP, ENDLOOP, WAIT, а так же числа. Переводы строк и пробелы игнорируются. Ниже содержание файла описания токенов для Lex (как водится, слева регулярное выражение, справа шаблон Delphi-кода для его обработки).

D				[0-9]

%start
%%

  var result : integer;

{D}+(\.{D}+)?([Ee][+-]?{D}+)?	begin
				  val(yytext, yylval.yyCardinal, result);
				  if result=0 then
				    return(NUM)
				  else
				    return(ILLEGAL)
				end;

RED				begin
	   			    return(RED)
				end;

YELLOW				begin
	   			    return(YELLOW)
				end;

GREEN				begin
	   			    return(GREEN)
				end;

INIT				begin
	   			    return(INIT)
				end;

ENDINIT				begin
	   			    return(ENDINIT)
				end;

LOOP				begin
	   			    return(LOOP)
				end;

ENDLOOP				begin
	   			    return(ENDLOOP)
				end;

ON				begin
	   			    return(ON)
				end;

OFF				begin
	   			    return(OFF)
				end;

WAIT				begin
	   			    return(WAIT)
				end;

" "             		;

.				|
\n				;
\r				;

Затем нужно показать Yacc’у, по каким правилам из этих токенов формируется синтаксическое дерево программы (ниже фрагмент конфигурационного файла):

%token <Cardinal> NUM 
%token <TCustomLight> RED YELLOW GREEN
%token <TEventList> INIT ENDINIT LOOP ENDLOOP

%type <TCustomEvent> expr
%type <TCustomLight> light

%token ON OFF
%token WAIT

%token ILLEGAL 		/* illegal token */

%%

input	:  /* empty */
	|  _init _loop		 { yyaccept; }   
	;

_init   : INIT exprs ENDINIT 	 { FInitList.AddRange(FTemporaryList.ToArray); FTemporaryList.Clear; }
        ;

_loop   : LOOP exprs ENDLOOP	 { FLoopList.AddRange(FTemporaryList.ToArray); FTemporaryList.Clear }
        ;

exprs   :  /* empty */
	|  exprs expr 		 { FTemporaryList.Add($2); }
        ;

expr    :  WAIT NUM		 { $$ := TWaitEvent.Create($2); }
	|  light ON		 { $$ := TLightOnEvent.Create($1); }
	|  light OFF		 { $$ := TLightOffEvent.Create($1); }
	;

light   :  RED			 { $$ := FRedLight; }
        |  YELLOW		 { $$ := FYellowLight; }
        |  GREEN		 { $$ := FGreenLight; }
	;

Здесь видно, что есть тип токена light, который олицетворяет собой название конкретной лампочки. Тип expr, представляющий собой отдельное действие над лампой или задержку. А так же списки этих действий (exprs, на которых построены блоки _init и _loop). И опять же — слева описание, справа в скобках шаблон кода. Для программы из примера по этому описанию парсер может сформировать примерно вот такое синтаксическое дерево и обработать его:

Синтаксическое дерево программы (TrafficLight)

На нижнем уровне находятся отдельные команды и их операнды, выше они постепенно объединяются в единое синтаксическое дерево. Проходя дерево «снизу вверх», останавливаясь в вершинах и выполняя код, указанный в шаблонах, парсер формирует результат. В нашем случае результат — это просто два списка команд (INIT и LOOP), эти списки затем используются для управления моделью светофора.

При должной сноровке подобные парсеры можно создавать очень и очень быстро. Не считая конфигурации утилит, в данной программе мною написано всего-лишь порядка 200 строк кода (включая модель светофора и код, который ей управляет). Это довольно мало, то есть разработка небольшого языка не требует больших затрат сил и времени. Хотя, конечно, необходима несколько большая квалификация, чем при работе с XML. Удобство пользователей обычно того стоит :)

Данный пост не претендует на то, чтобы быть полным руководством. Его цель продемонстрировать возможности утилит и относительную простоту разработки небольшого предметно-ориентированного языка, и всем этим, вероятно, заинтересовать тех, кто с этой темой в силу обстоятельств еще не сталкивался.

Ссылка на полный код примера.

Связанные записи: