📜 ⬆️ ⬇️

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

In the previous article, I talked a little about haxe - a simple and convenient general purpose language. However, besides simplicity and clarity, there are deep things in it - such as the concept of macros - code that is executed during the compilation process. Why in haxe there are no traditional C-like macros and what possibilities we tear off haxe-macros, and the article will be discussed.


Why are there no C-like macros in haxe?


In C, macros are used quite widely: this is for you and constants, these are definitions for conditional compilation, these are inline functions. They greatly increase the flexibility of the language. However, as a result of mixing, you will agree, 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 haxe creator Nicolas Kanassier decided to divide this concept into components. Thus, in the language appeared separately: constants (static inline var), definitions for conditional compilation (defines) and inline-methods.

Meanwhile, macros in haxe ...


Macros in haxe stand apart - I have not seen this in other languages. Briefly about them you can say the following:

What exactly can you do with macros? The following moments come to mind:

')

Macros in practice


The author has repeatedly resorted to using macros in real-world tasks. One has only to remember that macros add complexity to the code, which means they should be used only where it is really needed. In the standard haxe libraries, they have built a system for interacting with SPOD databases. The macros in it allow you to write queries to the database using a 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 redefined in descendants. The problem with this method is the formal return type. On the one hand, the method defined in the base class should return an object of the “base” type, on the other hand, it should ideally return an object of the type “descendant” redefined in descendants. Thus, the method signature is broken and all typed languages ​​known to me (including haxe) will stop trying to override the method with a different 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 is better to immediately place it in a separate file (this will make it easier for the compiler to separate the macro code from the usual one and thus avoid a number of problems, especially since cloning is usually needed in different places and therefore it would be good to have a macro method without being tied to the class to be cloned). 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 examine Base from Clonable and we will be able to 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) braces around the bodies of the methods and returnable data types for the methods (since the compiler will output them itself).
  2. Unfortunately, I did not manage to make it so that instead of “cloneTyped” we could write just “clone” (perhaps this point can be corrected).
  3. The cloneTyped () method will not be in the result code, nor will its calls (instead of cloneTyped () calls, there will be clone () calls with reduction to the required type without a drop in the program operation speed).
  4. For those who are interested in macros, after the first acquaintance, I recommend reading about reification (“materialization”) - ways to create a haxe-code from a macro for substitution instead of a call, directly writing it down.

findings


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

Source: https://habr.com/ru/post/245617/


All Articles