Макросы в haxe: выполняем код прямо во время компиляции (и это нормально)

    В предыдущей статье я немного рассказал про haxe — простой и удобный язык общего назначения. Однако, кроме простоты и понятности, есть в нём и вещи глубокие — такие, как концепция макросов — кода, который выполняется в процессе компиляции. Почему в haxe нет традиционных Си-подобных макросов и какие возможности нам отрывают haxe-макросы, и пойдёт речь в статье.


    Почему в haxe нет Си-подобных макросов?


    В Си макросы используются довольно широко: это тебе и константы, это и определения для условной компиляции, это и инлайн-функции. Они здорово повышают гибкость языка. Однако, как результат смешения, согласитесь, довольно разного функционала в одной концепции, макросы с Си несут с собой и трудности понимания исходного текста. Как мне кажется, именно поэтому создатель haxe Николя Канасье решил разделить эту концепцию на составляющие. Таким образом, в языке появились отдельно: константы (static inline var), определения для условной компиляции (defines) и inline-методы.

    А тем временем макросы в haxe...


    Макросы в 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());
    	}
    }
    

    Несколько замечаний:
    1. В коде я убрал необязательные (для случая одного выражения) фигурные скобки вокруг тел методов и возращаемые типы данных для методов (т.к. компилятор выведет их сам).
    2. К сожалению, мне не удалось сделать так, что вместо «cloneTyped» мы могли писать просто «clone» (возможно, этот момент можно исправить).
    3. Метода cloneTyped() не будет в результирующем коде, как и его вызовов (вместо вызовов cloneTyped() будут вызовы clone() с приведением к нужному типу без падения скорости работы программы).
    4. Тем, кто заинтересовался макросами, после первого знакомства, рекомендую почитать про reification («овеществление») — способы создавать haxe-код из макроса для подстановки вместо вызова, напрямую его записывая.

    Выводы


    Макросы в языке haxe — достаточно уникальная и мощная штука, которую стоит использовать там, где обычные способы не работают. Относительно несложно создавать/использовать макросы самому, однако не стоит забывать про готовые библиотеки (см. http://lib.haxe.org/t/macro). Надеюсь, материал был вам интересен.
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 10

      +2
      Таким образом, сигнатура метода нарушается и все известные мне типизированные языки (в том числе и haxe) пресекут попытку переопределить метод с другим возвращаемым типом.

      C++ не пресечёт. Виртуальная функция в наследуемом классе может возвращать указатель не на тот же тип, что возвращается в базовом, а на наследника возвращаемого типа (не обязательно того же типа, что и сам класс и его наследник).
        +3
        может возвращать указатель
        Для полноты: либо ссылку.

        Java, кстати, тоже поддерживает ковариантность типов возвращаемых значений. И C#, скорее всего, аналогично.
          0
          Такая подойдет? www.lwithers.me.uk/articles/covariant.html
            +3
            Такая подойдет?
            Вы меня не так поняли. Можно возвращать ссылку на объект (для них ковариантность тоже работает), я не про ссылки на статьи.
              +1
              Да, я вас не правильно понял, извините.
            +2
            C# так не может — но у него есть расширения, которыми так же можно решить эту задачу.
          +1
          По ссылке lib.haxe.org/t/macro, к сожалению, нету ссылок на проекты Димы Гранецкого — github.com/profelis/bindx2, github.com/profelis/halk, github.com/profelis/overload-operator.

          Также стоит отметить что он написал серию обучающих статей о макросах в Haxe: haxe.ru/taxonomy/term/455
            +1
            Очень приятно видеть язык с приличными (а не урезанными) макросами, но очень удивляет пассаж «мне не доводилось видеть такого в других языках», потому что если судить по описанию, то это немного ухудшенная версия хорошо известной «молодой» технологии (ей всего-то полвека, LOL). Или я чего-то не понимаю?
              0
              Да, вы правы, как-то я пропустил макропрограммирование для лиспа. Если бы не ваш комментарий, может и не узнал бы никогда что такое есть :) Спасибо!
              +4
              Кроме Lisp: D, Rust, Scala, Ocaml, и прочие.

              Only users with full accounts can post comments. Log in, please.