Apply @:genericBuild on a function in Haxe

Acknowledgement: The solution about applying @:genericBuild on functions is actually inspired by the haxetink libraries, in particular tink_json.

@:genericBuild in a nutshell

In Haxe, @:genericBuild is a metadata that tags a class to be built with a macro, which is invoked for every occurrence of the class.

chameleon-226287_640

// ThreeDArray.hx
@:genericBuild(MyMacro.build())
class ThreeDArray<T> {}

// MyMacro.hx
class MyMacro {
  public static function build() {
    switch haxe.macro.Context.getLocalType() {
      case TInst(_, [p]):
        var complexType = haxe.macro.TypeTools.toComplexType(p);
        return macro:Array<Array<Array<$complexType>>>;
      default: throw 'assert'; // technically unreachable
    }
  }
}

// Then in somewhere:
var my3dIntArray:ThreeDArray<Int>;
var my3dStringArray:ThreeDArray<String>;
// etc...

In the above example, ThreeDArray is tagged with the @:genericBuild metadata. That means the compiler will invoke the build macro for each occurrence of ThreeDArray. In our build macro, we define a 3D array of the type specified by the type parameter. So my3dIntArray will actually be Array<Array<Array<Int>> and my3dStringArray will actually be Array<Array<Array<String>>

Apply @:genericBuild on functions

I had this question in mind back in 2014. I was trying to write a generic utility class that clones objects. But I am unsatisfied with runtime-reflection-based ways (e.g. Reflect.copy() and Type.createEmptyInstance(), etc) because they are not performant and it is hard to precisely control the behaviour. So I was looking for a way to write generic functions that would have difference implementation depending on the type of the arguments:

class Cloner {
  public static function clone<T>(value:T):T {
    // I want to generically build the body of this method
    // based on the type parameter T
  }
}

In the above code, I would like to generate the function body differently for each type I pass in as argument. For example:

class Foo {
  public var fooValue:Int = 0;
  public function new() {}
}
typedef Bar = {
  barValue:Int,
}


var foo = new Foo();
foo.fooValue = 1;
var bar = {barValue: 2}

var clonedFoo = Cloner.clone(foo); // will have a function body like this: `{var cloned = new Foo(); cloned.fooValue = v.fooValue; return cloned;}`
$type(clonedFoo) // should be: Foo
trace(clonedFoo.fooValue) // should be: 1
Cloner.clone(bar); // will have a function body like this: `return {barValue:v.barValue};`
$type(clonedBar) // should be: Bar
trace(clonedBar.fooValue) // should be: 2

The above described what I wished, but there is no way to apply @:genericBuild on a function directly to achieve that (at least up to today at Haxe 3.3).

The solution

Although we cannot directly apply @:genericBuild on functions, the above use case can be perfectly achieved by combining expression macros and class built with @:genericBuild.

First, we prepare the generically built class. It will accept one type parameter and a class will be built with a clone function that is specific to that type.

// ClonerBase.hx
@:genericBuild(MyMacro.build())
class ClonerBase<T> {}

// MyMacro.hx
class MyMacro {
  public static function build() {
    switch haxe.macro.Context.getLocalType() {
      case TInst(_.get() => cls, [p]):
        // here we name our generically built class with 
        // the signature (hash) of the type to be cloned
        // in real code one should be careful about name clashes
        var clsname = 'Cloner_' + haxe.macro.Context.signature(p);

        // prepare the type to be cloned in form of ComplexType
        var complexType = haxe.macro.TypeTools.toComplexType(p);

        // depending on the type, prepare the actual cloning expressions, 
        // actual implementation omitted here for simplicity
        // but the result would be something like `var cloned = new Foo(); cloned.fooValue = v.fooValue; return cloned;`
        // as described above, but different for each input type
        var expr = prepareCloneExpr(p);

        // build and return the type
        var def = macro class $clsname {
          public function new() {}
          public function clone(v:$complexType):$complexType {
            $expr;
          }
        }
        haxe.macro.Context.defineType(def);
        return haxe.macro.Context.getType(clsname);

      default: throw 'assert'; // technically unreachable
    }
  }
}

Now we have built the actual cloner with @:genericBuild and we can use it right away already and it will give us the desired result as described in the previous section.

var clonedFoo = new ClonerBase<Foo>().clone(foo);
var clonedBar = new ClonerBase<Bar>().clone(bar);

But we wish to have a single static function to do the job: Cloner.clone(). In order to achieve that, we make Cloner.clone() a macro function which will give us the above expressions depending of the type of the cloned value:

class Cloner {
  public static macro function clone(e:haxe.macro.Expr) {
    var type= haxe.macro.Context.typeof(e);
    var complexType = haxe.macro.TypeTools.toComplexType(type);
    return macro new ClonerBase<$complexType>().clone($e);
  }
}

The above macro function will first try to figure out the type you are going to clone, and return an expression that create a new ClonerBase (which will in turn be generically built for the type of the cloned value) and then call the clone function of it. So now we can write what we initially wanted!

var clonedFoo = Cloner.clone(foo);
var clonedBar = Cloner.clone(bar);

Conclusion

Combining expression macro and class with @:genericBuild is a very powerful tool, especially for functional programming. Give it a try on your own codebase today!

More readings about the topic:

3 thoughts on “Apply @:genericBuild on a function in Haxe

  1. Hello! Your blog post explained better than haxe manual how to use genericbuild. One question though – you do

    case TInst(_.get() => cls, [p]):
    // here we name our generically built class with
    // the name of the type to be cloned
    // in real code one should be careful about name clashes
    var clsname = 'Cloner_' + cls.name;

    Hower this would create “Cloner_ClonerBase”, not expected “Cloner_String” or something.
    I did

    var clsname = '';

    switch(Context.getLocalType()) {
    case TInst(_.get() => cls, [p]): {
    clsname += '${cls.name}_';
    switch(p) {
    case TInst(_.get() => tcls, []): {
    clsname += tcls.name;
    }
    default: throw 'assert';
    }
    trace(clsname);

    What am I doing wrong?

    1. I have edit the code and it should work now. Basically I used Context.signature to generate a unique hash for the type. For your problem, it fails because you only handle the case for class instances (TInst), but Bar is not a class here, it is a anonymous object (TAnonymous).

Leave a Reply

Your email address will not be published. Required fields are marked *