macros (매크로)

C와 같은 언어에서는 사용자가 문법을 간단히 정의할 수 있도록 해주는 #define같은 기능이 있습니다. 그것들은 pseudo-code-generation(가상 코드 생성)를 수행하는데 유용하고 언어의 문법을 수정하도록 해주는 반면, 다른 개발자가 읽기힘든 코드를 만듭니다.

Haxe 매크로 시스템은 Haxe 문법을 수정하지 않으면서 컴파일 도중 코드생성을 해주는 강력한 기능입니다.

Macro functions

매크로는 컴파일 도중에 실행되는데, 값을 반환하는 대신에 컴파일 될 Haxe코드 조각을 반환하는 것이 매크로의 원리입니다.

@:macro Metadata를 사용하는 매크로 함수로 정의하면 됩니다.

아래는 빌드 날짜를 컴파일하는 매크로 예제입니다. 매크로는 컴파일 도중에 실행된다는 점을 유의해야합니다. 따라서 프로그램이 실행되는 '현재 날짜'가 아니라, 출판될 때의 날짜가 출력됩니다.

import haxe.macro.Context;
class Test {
    @:macro public static function getBuildDate() {
        var date = Date.now().toString();
        return Context.makeExpr(date, Context.currentPos());
    }
    static function main() {
        trace(getBuildDate());
    }
}

모든 매크로 함수는 그 매크로 호출를 대체할 코드블록에 상응하는 표현식(expression)을 반환해야 하므로, date 변수에 저장된 String 을 그에 상응하는 string-표현식으로 변환할 필요가 있습니다. Context.makeExpr이 그 역할을 합니다.

모든 표현식은 (에러 출력과 디버깅을 목적으로) 그것이 선언된 파일과 행을 알려주는 위치(position)가 필요합니다. 이 예제에서는 Context.currentPos() 위치가 getBuildDate() 매크로 호출의 위치입니다.

You cannot have platform specific imports in a file that has @:macro, unless you use some #if !macro ... #end wrappers around them. So put your macros in a specific macro class and do not try to create them next to your regular code unless you sure your not using platform specific code which I guess is quite rare, so simpler to always keep them separate.

Macro Reification

물론 간단한 값을 표현식으로 전환하는 것보다 더 많은 것을 할 수 있습니다. macro reification을 사용하면, 표현식을 생성하고 다룰 수 있습니다 :

import haxe.macro.Expr;
class Test {
    @:macro public static function repeat(cond:Expr,e:Expr) : Expr {
        return macro while( $cond ) trace($e);
    }
    static function main() {
        var x = 0;
        repeat(x < 10, x++);
    }
}

이 매크로는 while( x < 10 ) trace(x++) 과 같은 코드를 생성할 것 입니다.

A few explanations :

  • 매크로 repeat표현식를 인자로 가져오므로, 연산이 되지 않은 표현식의 형태로 받습니다. 이 표현식은 유효한 Haxe 문법이어야 하며, 선언되지 않은 변수/타입 등등을 참조할 수 있습니다.
  • 그 다음에 이런 표현식을 조작하고(아래를 보시길), 어떤 wrapping code에 의해 재사용합니다.
  • macro 키워드는 표현식을, 실행에 필효한 코드가 아니라 표현식을 생성할 코드로 다루게 됩니다. It will also replace all $-prefixed identifiers by the corresponding variable.

Reification Escaping

new in haxe 3.0

macro reification를 사용할 때, 표현식에 어떤 값을 주입하기 원한다면, 몇가지 방법이 있습니다.

  • ${value}를 사용하면, 그 지점의 표현식을 동일한 값으로 바꿉니다. The value needs to be an actual haxe.macro.Expr

    var v = macro "Hello";
    var e = macro ${v}.toLowerCase();
    // is the same as :
    var e = macro "Hello".toLowerCase();
  • In some case where the value can only be an identifier and not an expression, $ident' is replaced by the value of the identifier :

With var:

    var myVar = "i";
    var e = macro var $myVar = 0;
    // is the same as :
    var e = macro var i = 0;

With field :

    var myField = "f";
    var e = macro o.$myField;
    // is the same as :
    var e = macro o.f;

With object fields :

    var myField = "f";
    var e = macro { $myField : 0 };
    // is the same as :
    var e = macro { f : 0 };

  • Using $i{ident}, will create an identifier which value is ident
        var varName = "myVar";
        var e = macro $i{varName}++;
        // is the same as :
        var e = macro myVar++;
  • Using $v{value} will tranform the value into the corresponding expression, in a similar way to Context.makeExpr :
        var myStr = "some string";
        var e = macro $v{myStr};
        // is the same as :
        var e = macro "some string";

    And for a more complex case :
        var o = { x : 5 * 20 };
        var e = macro $v{o};
        // is the same as :
        var e = macro { x : 100 };
  • Using $a{exprs}, will substitute the expression Array in a {..} block, a [ ... ] constant array or a call parameters :
        var args = [macro "sub", macro 3];
        var e = macro "Hello".toLowerCase($a{args});
        // is the same as :
        var e = macro "Hello".toLowerCase("sub",3);

Creating Expressions

As we see in the two last examples, we have several ways of creating expressions :

  • using Context.makeExpr to convert a value into the corresponding expression, that - when run - will produce the same value
  • using macro to convert some Haxe code into an expression
  • expressions can also be created "by-hand", since they are just plain Haxe enums :

// manual creation by using enums :
var e : Expr = {
    expr : EConst(CString("Hello World")), 
    pos : Context.currentPos()
};
// is actually the same as :
var e : Expr = macro "Hello World !";

Manipulating expressions

Since expressions are just a small structure with a position and an enum, you can easily match them.

For instance the following example make sure that the expression passed as argument is a constant String, and generates a string constant based on the file content :

import haxe.macro.Expr;
import haxe.macro.Context;
class Test {
    @:macro static function getFileContent( fileName : Expr ) {
        var fileStr = null;
        switch( fileName.expr ) {
        case EConst(c):
            switch( c ) {
            case CString(s): fileStr = s;
            default:
            }
        default:
        };
        if( fileStr == null )
            Context.error("Constant string expected",fileName.pos);
        return Context.makeExpr(sys.io.File.getContent(fileStr),fileName.pos);
    }
    static function main() {
        trace(getFileContent("myFile.txt"));
    }
}

Please note that since macro execute at compile-time, the following example will not work :

var file = "myFile.txt";
getFileContent(file);

Because it that case the macro fileName argument expression will be the identifier file, and there is no way to know its value without actually running the code, which is not possible since the code might use some platform-specific API that the macro compiler cannot emulate.

Constant arguments

The above example can be greatly simplified by telling that your macro only accept constant strings :

@:macro static function getFileContent( fileName : String ) {
    var content = sys.io.File.getContent(fileName);
    return Context.makeExpr(content,Context.currentPos());
}

Again - same as above - you will have to pass a constant expression, it cannot be a value of type String.

The following types are supported for constant arguments :

  • Int, Bool, String, Float
  • arrays of constants
  • structures of constants
  • null value

Context API and macro Context

The Context class gives you access to a lot of informations, such as compilation parameters, but also the ability to load type or even create new classes.

The Context API operates on the "application context", which is the unit in which the compilation of your code occurs. There is another context which is the "macro context" which compiles and run the macro code. Please note that these two contexts are separated.

For instance, if you compile a Javascript file :

  • the application context will contain your Haxe/Javascript code, it will have access to the Javascript API
  • the macro context will contain your macro code (the classes in which @:macro methods are declared) and it will not be able to access the Javascript API. It can however access the Sys API, the sys package and the neko API as well. It can still interact and modify the application context by using the Context class.

It is important to understand that some code sometimes get compiled twice : once inside the application context and once inside the macro context.

In general, it is recommended to completely separate your macro code (classes contaiting @:macro statements) from your application code, in order to prevent expected issues due to some code being included in the wrong context.

Type Reification

It is possible to use macro : Type to build a type instead of an expression.

Type reification also support escaping (see below).

The following example will declare a new typed variable when called :

import haxe.macro.Expr;
class Test {
    @:macro static function decl( vname : String ) {
        var str : ComplexType = macro : String;
        var arr : ComplexType = macro : Array<Array<$str>>;
        return macro var $vname : $arr = [];
    }
    #if !macro
    static function main() {
        decl("table");
        trace(table);
    }
    #end
}

Please note that in that case we need to make sure that the main() code is not compiled as part of the macro context.

Building types

You can also generate and manipulate types declarations content with macros, see Building Types with Macros

Advanced Features

Read on to know if you want to learn every bit about Haxe macro possibilities : Advanced Macro Features

version #20119, modified 2014-05-22 11:25:38 by papapang