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.
// 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:
Thanks for this! I was thinking about implementing something like that for a while in my project, but never had time to think it through and then this article appears 🙂
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?
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).Hey dude if I paid you would you give me some lessons in HAXE Specifically for my use case, using your Pathfinding class if it’s okay to use it? Please e-mail me or leave a response here. I think you can see my e-mail here no?