Haxe — очень удобный и практичный язык, но маленькое сообщество и, как результат, небольшое количество библиотек заставляют меня немало времени тратить на подготовку «заголовочных файлов» для интеграции open source библиотек в haxe. Немного об этом языке и о путях преобразования исходного кода на разных языках мне бы и хотелось рассказать ниже.
С языком программирования haxe (тогда ещё его звали haXe) я познакомился около трёх лет назад и с тех пор мы не расстаёмся. Т.к. этот язык мало освещён на Хабре, то для начала — о haxe «in a nutshell», как поётся в известной песне.
О haxe в двух словах
Для тех, кто незнаком с haxe, чуть-чуть вводной информации:
- синтаксис этого языка почти полностью повторяет ActionScript, который в свою очередь похож на JavaScript, но с типами данных;
- жёсткая типизация, но с автоматическим выводом типов (для простых случаев);
- отсутствие жёстко привязанной среды выполнения — компилятор лишь транслирует haxe-код в другие языки (сейчас поддерживаются: neko, php, javascript, flash/actionscript, c++, java, c#; на подходе также python);
- очень быстрый компилятор.
Как и другие языки, haxe не является серебряной пулей и, как мне кажется, есть две основные области, где он полезен:
- написание мультиплатформенных приложений (здесь стоит упомянуть библиотеку для разработки игр OpenFL);
- написание сложных js-приложений (т.к. их написание сразу на js проблемно ввиду отсутствия типизации).
Конвертируем код
Пути для преобразования исходного кода на одном языке в код на другом языке я вижу следующие:
- через построение полноценного дерева разбора (Abstract Source Tree = AST);
- через использование инструментов, умеющих преобразовывать исходные коды во что-то более простое (наподобие xml);
- «грубой силой» через использование регулярных выражений.
Без сомнения, математически верный путь — первый, т.к. позволяет сделать всё аккуратно и, в идеале, получить на выходе сразу компилируемый текст на другом языке. Минусы — полноценный разбор сложен, чувствителен к деталям. Почитать про построение AST-деревьев можно в литературе по компиляторам (см., например, Ахо А., Сети Р., Ульман Дж., Лам М. — Компиляторы. Принципы, технологии, инструменты).
Второй путь возможен только при наличии подходящих утилит для исходного языка. Автору доводилось использовать yuidoc при написании генератора haxe-обёртки для популярной js-библиотеки easeljs, благо последняя хорошо документирована.
Третий путь — через обработку регулярными выражениями — относительно прост, хотя и требует «доводки напильником» результирующего кода. Именно об этом варианте пойдёт речь ниже.
Окей, регулярка, конвертируй!
Регулярные выражения имеют огромный, на мой взгляд плюс — быстро пишутся и, всего лишь, пару минусов:
- в принципе не могут разобрать вложенные (рекуррентные) структуры (с произвольным уровнем вложенности);
- тяжело читаются (а для больших выражений — и не менее тяжело пишутся).
Первый недостаток, как показала практика, для большинства языков не очень критичен, особенно если нам не нужно полноценное преобразование, а нужно лишь «выдёргивание» заголовков классов и методов. Второй же можно частично обойти введя константы, хранящие небольшие кусочки регулярных выражений и позволив использовать их для написания более сложных конструкций.
В результате приходим к наборам правил преобразования, где есть два вида этих самых правил: объявления констант и регулярные выражения для поиска/замены. Вот фрагмент файла правил для преобразования из c# в haxe:
ID = b[_a-zA-Z][_a-zA-Z0-9]*b
LONGID = ID(?:[.]ID)*
TYPE = LONGID(?:[<]s*LONGID(?:s*,s*LONGID)*s*[>])?
// "int[]" => "Array<int>"
/(TYPE)s*[s*]/Array<$1>/
// "int v" => "var v:int"
/(TYPE)s+(ID)/var $2:$1/
Дело остаётся за малым — написать инструмент, который бы принимал на вход файлы с исходными текстами и файл regex-правил, а на выходе выдавал бы файлы с результатом применения этих правил. И такая утилита была написана (refactor). Ниже я приведу немного кода, чтобы показать (я надеюсь) простоту и лаконичность языка haxe.
Рассмотрим код класса, читающего файл с правилами, разбирающего — где константы, а где регулярки, и строящего массив регулярных выражений для последующего преобразования исходных файлов:
import stdlib.Regex; // используем класс Regex из библиотеки stdlib, т.к. стандартный EReg недостаточно умный в смысле замены
import sys.io.File;
// using ниже подмешивает static-методы класса StringTools ко всем строкам;
// например, у класса String нет метода replace();
// мы могли бы писать StringTools.replace("abc", "b", "z"), но благодаря using можем писать "abc".replace("b", "z");
// однако, всё это лишь сахар - при компиляции сгенерируется код с обычным вызовом static метода
using StringTools;
class Rules
{
// здесь (default, null) говорит о том, что мы объявляем переменную,
// которую позволительно читать извне (default), а вот менять - нельзя (null);
// бывает ещё "never" - когда нельзя делать операцию не только извне, но и внутри класса
public var regexs(default, null) : Array<Regex>;
public function new(rulesFile:String)
{
var text = File.getContent(rulesFile); // тип для text будет выведен автоматически
regexs = [];
var lines = text.replace("r", "").split("n"); // тип данных для lines не указываем, компилятор выведет сам
var consts = new Array<{ name:String, value:String }>(); // также тип данных легко выводится автоматически
// for в haxe только такой - в формате foreach;
// перебрать числа от 0 до 9 можно так: for (n in 0...10)
for (line in lines)
{
line = line.trim();
if (line == "" || line.startsWith("//")) continue;
var reConst = ~/^([_a-zA-Z][_a-zA-Z0-9]*)s*[=]s*(.+?)$/; // регулярка для детектирования константы
if (reConst.match(line))
{
var value = reConst.matched(2);
for (const in consts)
{
value = replaceWord(value, const.name, const.value);
}
consts.push({ name:reConst.matched(1), value:value });
}
else
{
for (const in consts)
{
line = replaceWord(line, const.name, const.value);
}
regexs.push(new Regex(line.replace("t", "")));
}
}
}
// метод меняет константу на её значение
static function replaceWord(src:String, search:String, replacement:String) : String
{
var re = new EReg("(^|[^_a-zA-Z0-9])" + search + "($|[^_a-zA-Z0-9])", "g");
// map() ищет в строке src по регулярному выражению
// и меняет найденное на результат выполнения функции, переданной ему вторым параметром
return re.map(src, function(re)
{
return re.matched(1) + replacement + re.matched(2);
});
}
}
Заключение
Автор уже три года использует haxe для написания web-приложений. Это здорово: возможность писать код клиента и сервера на одном языке + строгая типизация + синтаксис, близкий к js — всё это очень радует.
Созданный инструмент refactor упростил интеграцию haxe-кода со сторонними библиотеками. Например, недавно с его помощью была создана обёртка для js-библиотеки threejs.
Надеюсь, мне удалось вас заинтересовать если не языком haxe, то хотя бы подходом к обработке исходных текстов программ. Ведь при помощи этого простого метода можно не только конвертировать программы с языка на язык, но и просто делать текст программы красивым (beautify).
Автор: yar3333