Макросы в haxe: выполняем код прямо во время компиляции (и это нормально)
В предыдущей статье я немного рассказал про haxe — простой и удобный язык общего назначения. Однако, кроме простоты и понятности, есть в нём и вещи глубокие — такие, как концепция макросов — кода, который выполняется в процессе компиляции. Почему в haxe нет традиционных Си-подобных макросов и какие возможности нам отрывают haxe-макросы, и пойдёт речь в статье.В Си макросы используются довольно широко: это тебе и константы, это и определения для условной компиляции, это и инлайн-функции. Они здорово повышают гибкость языка. Однако, как результат смешения, согласитесь, довольно разного функционала в одной концепции, макросы с Си несут с собой и трудности понимания исходного текста. Как мне кажется, именно поэтому создатель haxe Николя Канасье решил разделить эту концепцию на составляющие. Таким образом, в языке появились отдельно: константы (static inline var), определения для условной компиляции (defines) и inline-методы. Макросы в haxe стоят особняком — мне не доводилось видеть такого в других языках. Коротко о них можно сказать следующее: пишутся на haxe; формально они — статические и нестатические методы обычных классов; эти методы могут иметь входные параметры, а возвращают — произвольное выражение; это выражение подставляется в место вызова макроса; внутри: компилируются в neko, а затем выполняются перед началом компиляции остального кода программы; поэтому могут всё, что может neko (читать/писать произвольные файлы, обращаться к БД и прочее). Что именно можно делать с помощью макросов? В голову приходят следующие моменты: изменять содержимое классов — вставлять новые переменные и методы, менять типы данных и проч. разворачивать код во что-то более сложное; менять метаинформацию — помечать классы/методы/переменные для использования этого впоследствии для рефлексии; менять определения условной компиляции (т. е. ваша программа, например, сможет по-разному собираться в зависимости от внешних условий; например, можно собирать development или production версию в зависимости от значения в файле конфигурации); добавлять новые типы данных, удалять существующие, обрабатывать все типы (например, это позволяет сделать генератор документации); макросы в виде статических методов могут вызываться из опций командной строки компилятора (есть, например, уже готовые макросы для импорта заданных папок с классами, для исключения из компиляции определённых файлов и ещё нескольких более специфических функций). Автор неоднократно прибегал к использованию макросов в реальных задачах. Стоит лишь помнить, что макросы добавляют коду сложности, а значит их стоит использовать лишь там, где это действительно нужно. В стандартных библиотеках haxe на них построена система взаимодействия с базами данных SPOD. Макросы в ней позволяют писать запросы к БД, используя жёсткий синтаксис с автодополнением (наподобие LINQ в C#).Пример: метод clone () с подстраивающимся возвращаемым типом В типичной ситуации, метод clone () определяется у базового класса, а затем переопределяется в потомках. Проблема этого метода — формальный возвращаемый тип. С одной стороны, метод, определённый в базовом классе, должен возвращать объект типа «базовый», с другой стороны — переопределённый в потомках он должен, в идеале, возвращать объект типа «потомок». Таким образом, сигнатура метода нарушается и все известные мне типизированные языки (в том числе и haxe) пресекут попытку переопределить метод с другим возвращаемым типом. А ведь было бы хорошо, если бы можно было писать такой код: class Base { public function new () { } public function clone () : Base { return new Base (); } }
class Child extends Base { public function new () { super (); } override function clone () : Base { return new Child (); } public function childMethod () return «childMethod»; }
class Main { static function main () { // Хорошо бы, чтобы строка ниже не вызывала ошибки trace (new Child ().clone ().childMethod ()); } } Давайте напишем макрос-метод, который будет возвращать правильный тип. Лучше сразу поместить его в отдельный файл (этим мы упростим компилятору отделение макро-кода от обычного и избежим тем самым ряда проблем, тем более, что клонирование обычно нужно в разных местах и потому хорошо бы иметь макро-метод без привязки к клонируемому классу). Класс с макросом будет выглядеть так: // файл Clonable.hx import haxe.macro.Expr; import haxe.macro.Context; import haxe.macro.Type;
class Clonable { // в нестатические макросы первым параметром всегда отдаётся ссылка // на выражение, через которое был вызван этот метод macro public function cloneTyped (ethis: Expr) { var tpath = Context.toComplexType (Context.typeof (ethis)); // возвращаем код, который мы хотим подставить в место вызова данного макро-метода; // как и везде, без денег — ни куда — $ позволяет вставить значение локальной переменной return macro (cast $ethis.clone ():$tpath); } } Теперь достаточно пронаследовать Base от Clonable и мы сможем пользоваться методом cloneTyped (): // файл Main.hx class Base extends Clonable { public function new () { } public function clone () return new Base (); }
class Child extends Base { public function new () super (); override function clone () return new Child (); public function childMethod () return «childMethod»; }
class Main { static function main () { // Здесь, несмотря на то, что Child.clone () имеет формальный тип Base, // мы, однако, можем сделать вызов childMethod (), как если бы нам из clone () вернулся тип Child trace (new Child ().cloneTyped ().childMethod ()); } } Несколько замечаний: В коде я убрал необязательные (для случая одного выражения) фигурные скобки вокруг тел методов и возращаемые типы данных для методов (т.к. компилятор выведет их сам). К сожалению, мне не удалось сделать так, что вместо «cloneTyped» мы могли писать просто «clone» (возможно, этот момент можно исправить). Метода cloneTyped () не будет в результирующем коде, как и его вызовов (вместо вызовов cloneTyped () будут вызовы clone () с приведением к нужному типу без падения скорости работы программы). Тем, кто заинтересовался макросами, после первого знакомства, рекомендую почитать про reification («овеществление») — способы создавать haxe-код из макроса для подстановки вместо вызова, напрямую его записывая. Макросы в языке haxe — достаточно уникальная и мощная штука, которую стоит использовать там, где обычные способы не работают. Относительно несложно создавать/использовать макросы самому, однако не стоит забывать про готовые библиотеки (см. http://lib.haxe.org/t/macro). Надеюсь, материал был вам интересен.