Существует ряд языков программирования (ЯП), синтаксис которых либо допускает введение конструкций, определяемых пользователем; либо разрешает переопределение (доопределение) существующих элементов языка. Такого рода языки делают программирование еще более приятным и интересным, позволяя реализовывать идеи из любого ЯП.
Не знаю, как вам, а мне порой хочется иметь в своем любимом языке некоторые элементы, взятые из какого-то другого ЯП. Изучение других ЯП очень стимулирует, а к некоторым вещам просто возникает привычка, от которой никуда не денешься.
Хотелось бы, чтобы у D была расширяемость синтаксиса, но, к сожалению, ее нет…
Тем не менее, это не помешает ввести в D элемент, например, из Haskell, а все что для этого потребуется – умелая работа со строками и немного того, что принято называть «шаблонной магией»…
Ради интереса, попробуем добавить в D такую интересную штуку, как генератор списков (list comprehension) и которая нам известна по таким языкам программирования, как Python, Haskell и некоторые другие. В качестве образца возьмем синтаксис генератора списков из Haskell, т.к. его легче реализовать, и он более лаконичен.
Синтаксис генератора списков в общем виде выглядит примерно так:
[f(x) | x <- xs]
где f(x) – некоторая функция от элемента x, который берется из списка xs.
Результатом выполнения приведенного генератора будет новый список, элементы которого получаются путем применения функции f к элементам xs.
Чтобы было понятнее, можно предположить, что генератор списка берет некоторый массив, некоторую функцию и создает новый массив, который получен путем воздействия функции на каждый элемент исходного массива. Именно такое поведение, мы и будем реализовывать с помощью разбора строк и шаблонов: разработаем некоторый шаблон, который примет строку с выражением для генератора списков, переведет его в код на D и выполнит этот код, возвратив результат.
Первое, что придется сделать – это разобрать строку с выражением генератора и сгенерировать код на D. Заметим, что выражение заключено в квадратные скобки, которые обозначают, что сам генератор списка дает массив. Поэтому перед разбором строки необходимо удалить эти скобки, а перед их удалением, требуется избавиться от начальных и конечных пробелов в строке выражения (это достигается использованием функции strip из std.string). После удаления квадратных скобок, выражение состоит из двух частей: выражение для функции (т.е. по сути ее тело без сигнатуры и оператора return) и выражения, описывающего взятие элемента из некоторого списка (оно состоит по сути дела из некоторой внутренней переменной и выражения, описывающего сам список). Эти две составные части генератора списков разделены только знаком “|” и больше ничем (кроме пробелов, разумеется).
Этим фактом мы и воспользуемся: с помощью split из std.string разобъем строку по разделителю “|”, уберем начальные и концевые пробелы из двух получившихся частей. Первая часть уже содержит одно из интересующих нас выражений (а именно, тело функции), а вот второе нужно разбирать отдельно…
Почему так? Потому, что из второго выражения можно получить имя переменной и выражение для исходного списка. Стоит заметить, что переменная отделена от списка только символами “<-”, по которым и осуществим разбиение, после которого над каждой частью выполним strip. Теперь у нас есть: имя переменной из функции, функция и сам список, вопрос только в том, как это оттранслировать в D. После некоторых раздумий и экспериментов, я пришел к выводу, что проще всего будет сформировать с помощью format строку следующего вида:
map!(имя_переменной => тело_функции).array
Это довольно простой и понятный способ, который можно реализовать вот так:
auto translateList(string expression)() { import std.algorithm; import std.array; import std.string; auto parsedExpression = expression.strip[1..$-1].split("|"); auto listExpression = parsedExpression[1].strip.split("<-"); string variableName = listExpression[0].strip; string functionBody = parsedExpression[0].strip; string sourceList = listExpression[1].strip; string constructedFunction = format( ` map!(%s => %s)(%s).array `, variableName, functionBody, sourceList ); return constructedFunction.strip; }
Кстати, один забавный момент: работает все это дело только во время компиляции, поскольку сама функция translateList является своеобразной параметризованной функцией, единственный параметр которой подается прямо во время компиляции.
Испытать функцию можно, например, так:
writeln(translateList!"[ 2 * temporary + 1 | temporary <- iota(0, 10) ]");
Результатом будет строка, в которую будет развернут генератор списков, в данном случае строка будет выглядеть так:
map!(temporary => 2 * temporary + 1)(iota(0, 10)).array
Выполнение строки с подстановкой результата выглядит несколько сложнее.
Введем вспомогательный эпонимичный (т.е. одноименный) шаблон следующего вида:
template evaluateForm(string functionalForm) { enum evaluateForm = functionalForm; }
Этот код работает очень просто: на место употребления шаблона просто вставляется некоторая (функциональная) форма из языка D, в роли которой может быть практически любое выражение (за некоторым исключением). Шаблон evaluateForm обеспечит лишь простую подстановку, но помимо этого, необходимо гарантированное выполнение оттранслированного в D генератора списка, чего шаблон evaluateForm не сможет сделать. Однако, применяя инструкцию mixin, можно выполнить любую валидную строку кода на D (есть целый ряд ограничений, который описан в документации).
Используя всю мощь этого языкового средства D, создадим наконец итоговый генератор списков, совместив все рассмотренные процедуры в одну:
auto overList(string expression)() { import std.algorithm; import std.range; return mixin(evaluateForm!(translateList!expression)); }
Работает это в свете уже рассмотренного весьма тривиально: шаблон evaluateForm подставляет в mixin инструкцию, которая была сгенерирована функцией translateList; после чего mixin выполняет сгенерированный во время компиляции D код.
Использовать полученную функцию overList очень просто:
auto f(int x) { import std.math; return sin(cast(double) x); } void main() { writeln(translateList!"[ 2 * x + 1 | x <- iota(0, 10) ]"); writeln(overList!"[ 2 * f(temporary) + 1 | temporary <- iota(0, 5) ]"); writeln(overList!`[ f(phi) | phi <- iota(0, 10)]`); writeln(overList!`[ 2 * x | x <- [1,2,3,4,5] ]`); }
Результат выглядит так:
[1, 2.68294, 2.81859, 1.28224, -0.513605] [0, 0.841471, 0.909297, 0.14112, -0.756802, -0.958924, -0.279415, 0.656987, 0.989358, 0.412118] [2, 4, 6, 8, 10]
Здорово, не правда ли?!
Однако, я в ходе реализации идеи нашел один неприятный момент, связанный с разработанной процедурой overList…
Оказалось, что любая функция из стандартной библиотеки (кроме функций из модулей std.algorithm и std.range), введенная в шаблон overList приведет к ошибке, в которой будет написано что-то про undefined.
Дело в том, как осуществляется подстановка результата mixin: то, что было в шаблоне на подстановку, то и подставится, иначе говоря, такое использование шаблона ограничено областью видимости выражения (а точнее, шаблона), попавшего во внутрь overList.
Казалось бы, на этом все, но я случайно нашел «грязный хак», который позволяет обойти ограничение: достаточно описать свою функцию, поместив в нее все нужные импорты или просто вставив в нее выполнение нужной функции из стандартной библиотеки, как например, это было в примере выше:
auto f(int x) { import std.math; return sin(cast(double) x); }
Помимо этого, есть еще одна нехорошая особенность overList – дело в том, что все эти манипуляции происходят на стадии компиляции. Впрочем, эта особенность – палка о двух концах, и в большинстве случаев, никакой разницы нет (однако, это не означает, что о том, как выполняется эта процедура следует забыть, в противном случае, можно нарваться на внезапные ошибки).
Резюмируя статью, скажу, что хоть D не является расширяемым языком, и тем более не является динамическим, но с помощью продвинутых средств метапрограммирования (выполнение функций на стадии компиляции, шаблоны, встраиваемый код, текстовый импорт и т.д.) можно эмулировать некоторое поведение других ЯП (в том числе, и динамических), а также создавать целые предметно-ориентированные языки (DSL = domain specific language, предметно-ориентированный язык) и препроцессоры!