Back to Home

Macros in haxe: execute code directly at compile time (and this is normal)

haxe · macros · macro · macros

Macros in haxe: execute code directly at compile time (and this is normal)

    In a previous article, I talked a bit about haxe, a simple and convenient general-purpose language. However, in addition to simplicity and clarity, there are also deep things in it - such as the concept of macros - code that is executed during compilation. Why there are no traditional C-like macros in haxe and what possibilities haxe-macros open for us, and the article will be discussed.


    Why is there no C-like macro in haxe?


    In C, macros are used quite widely: these are for you constants, these are definitions for conditional compilation, these are inline functions. They greatly enhance the flexibility of the language. However, as a result of mixing, you must admit, there are quite different functionalities in one concept, macros with C carry with them difficulties in understanding the source text. It seems to me that this is why the creator of haxe, Nicolas Kanasie, decided to divide this concept into components. Thus, the language appeared separately: constants (static inline var), definitions for conditional compilation (defines) and inline methods.

    Meanwhile, macros in haxe ...


    Macros in haxe stand out - I have never seen this in other languages. Briefly about them we can say the following:
    • written in haxe;
    • formally, they are static and non-static methods of ordinary classes;
    • these methods can have input parameters, and return an arbitrary expression;
    • this expression is substituted at the place of the macro call;
    • inside: compiled into neko, and then executed before compiling the rest of the program code;
    • therefore, everything that neko can do (read / write arbitrary files, access the database, etc.).

    What exactly can be done with macros? The following points come to mind:
    • change the contents of classes - insert new variables and methods, change data types and so on.
    • Deploy code to something more complex
    • change meta-information - mark classes / methods / variables for use later on for reflection;
    • change the definitions of conditional compilation (i.e., your program, for example, can be assembled differently depending on external conditions; for example, you can collect a development or production version depending on the value in the configuration file);
    • add new data types, delete existing ones, process all types (for example, this allows you to make a documentation generator);
    • macros in the form of static methods can be called from compiler command line options (for example, ready-made macros for importing specified folders with classes to exclude certain files from compilation and a few more more specific functions).


    Macros in practice


    The author has repeatedly resorted to the use of macros in real tasks. It is worth remembering that macros add complexity to the code, which means that they should be used only where it is really needed. In standard haxe libraries, they built a system for interacting with SPOD databases . The macros in it allow you to write database queries using the hard syntax with auto-completion (like LINQ in C #).

    Example: clone () method with adjustable return type


    In a typical situation, the clone () method is defined on the base class, and then overridden in descendants. The problem with this method is the formal return type. On the one hand, a method defined in a base class should return an object of type “base”, on the other hand, overridden in descendants, it should, ideally, return an object of type “descendant”. Thus, the signature of the method is violated and all the typed languages ​​I know (including haxe) will stop the attempt to override the method with another return type. But it would be nice if you could write such code:
    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());
    	}
    }
    

    Let's write a macro method that will return the correct type. It’s better to immediately put it in a separate file (this will simplify the compiler’s separation of the macro code from the usual and thereby avoid a number of problems, especially since cloning is usually necessary in different places and therefore it would be nice to have a macro method without binding to the cloned class). A class with a macro will look like this:
    // файл 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); 
    	}
    }
    

    Now it’s enough to inherit Base from Clonable and we can use the cloneTyped () method:
    // файл 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());
    	}
    }
    

    A few notes:
    1. In the code, I removed the optional (for the case of a single expression) curly brackets around the method bodies and the return data types for the methods (because the compiler will output them itself).
    2. Unfortunately, I could not make it so that instead of “cloneTyped” we could write simply “clone” (perhaps this point can be fixed).
    3. The cloneTyped () method will not be in the resulting code, as well as its calls (instead of cloneTyped () calls, there will be clone () calls with the conversion to the desired type without a drop in the speed of the program).
    4. For those who are interested in macros, after my first acquaintance, I recommend reading about reification (“reification”) - ways to create a haxe code from a macro to substitute instead of a call by writing it directly.

    conclusions


    Macros in the haxe language are a rather unique and powerful thing that should be used where conventional methods do not work. It is relatively easy to create / use macros yourself, but do not forget about ready-made libraries (see http://lib.haxe.org/t/macro ). I hope the material was interesting to you.

    Read Next