Help with Haxe macros.

221 views
Skip to first unread message

Mixer

unread,
Dec 16, 2015, 3:05:38 AM12/16/15
to Haxe
Hi there. I am new to macros, and was wondering how can I make a macro which converts a typical 'player' class

class player {
    @:save var health:Int = 10;
    @:save var position:Pos = new Position(10,14);
}


to a player class with a method that creates an object, with all fields marked with specific metadata...


class player {
    @:save var health:Int = 10;
    @:save var position:Pos = new Position(10,14);

    public function getSaveData (){
        return { "health":10, "position": x,y }
    }
}

So really, my question boils down to...
How can I get the name and value of all fields in a class marked with meta data
How can I generate a function in a macro

Thanks very much, hope you are able to help

Haxiomic

unread,
Dec 16, 2015, 8:58:17 AM12/16/15
to Haxe
Hey mixer, that's quite a good idea for a macro.

Macros took me a little while to grok but it was totally worth the effort when I did. I use them all the time now.

Macros are all about composing haxe expressions, so the best tip I can give for getting the hang is to keep a copy of std/haxe/macro/Expr.hx open so you can reference it and get a feel for how haxe expressions are structured. You also want to use ExprTools.toString() a lot so you can check to see if the haxe code is generated correctly!

What you want is a class building macro: a macro that alters the class at compile time. To use one you add @:build(...) meta data to the class and pass in a call to your class building function. If you want this build macro to apply to the subclasses of the class then you can use @:autoBuild meta. However! This _only_ applies to the subclasses, to apply to both the class and the subclass use both @build and @autoBuild.

Here's an implementation of the macro you're looking to build (I hope I understood correctly). I've annotated it as much as I could

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.ExprTools;

class ClassBuild{

//the entire contents of a class is contained in the class's fields
//a class-building-macro's job is to return an array of fields
static function saveClass():Array<Field>{

//we want to keep the currently defined fields intact, so first we get them from the context
var fields = Context.getBuildFields();

//the plan is to add a new field (which will be a function) named getSavedData
//we want to set the contents of that function to return an object containing all our @:save fields

var saveFieldNames = new Array<String>();

//first we find the @:save fields:
//iterate fields, look at field metas and find ones named :save
for(field in fields){

var metas = field.meta;//an array of meta objects (checkout std/haxe/macro/Expr to see the typedefs)
for(meta in metas){
switch meta.name {
case ':save':
//(we can also access any params here with meta.params)
//ok, we now know this field has @:save meta, lets store it's name in an array
saveFieldNames.push(field.name);
}
}

}

//now we want to do add a new field that looks like this
/*
public function getSavedData(){
return {
$fieldName1: $fieldName1,
$fieldName2: $fieldName2,
...
}
}
*/

//it's possible to compose the whole field manually using the typedefs in Expr.hx, but it's much easier to use the 'macro' keyword
//this takes expressions and turns them into Expr types for you

//the macro keyword pretty powerful but sometimes we do need to do things manually:
//the following creates an expression for an object like: {a: a, b: b}
var objExpr:Expr = {
expr: EObjectDecl([
for(name in saveFieldNames){
field: name,
expr: macro $i{name}
}
]),
pos: Context.currentPos()
}

//lets test to see if the expression looks right
//trace(ExprTools.toString(objExpr));

//we can't do 'macro public function getSavedData(){...}' because it's not supported
//what we can do however is use it to create a class expression and then just take the fields from that
//just like string interpolation, we can use the object expression we created above
var classObject = macro class SomeName{

public function getSavedData(){
return ${objExpr};
}

};

//now we append the fields in our 'classObject' to the fields of the class we're building
fields = fields.concat(classObject.fields);

//job done
return fields;
}

}


You can then use it like
@:build(ClassBuild.saveClass())
class Player{
    @:save var health:Float = 100.0;

...


Be careful, if you put ClassBuild in a folder/module, you need to reference the full path in the @:build meta ie:

@:build(module.ClassBuild.saveClass())

Best,
George

Haxiomic

unread,
Dec 16, 2015, 8:59:52 AM12/16/15
to Haxe

Mixer

unread,
Dec 17, 2015, 12:17:02 AM12/17/15
to Haxe
Oh my gosh, thank you for that! Thats a heap of help. Thankyou!!

Mixer

unread,
Dec 17, 2015, 12:19:42 AM12/17/15
to Haxe
Comments are legendary, this has really helped me understand macros.


On Thursday, December 17, 2015 at 12:59:52 AM UTC+11, Haxiomic wrote:

Mixer

unread,
Dec 17, 2015, 3:02:41 AM12/17/15
to Haxe
Thanks for this. One issue is that getSaveData() may often be nested, meaning that rather than putting the full object in, it should only get that returned by the childs 'getSaveData' method. In an attempt to solve this, I fiddled with changing expr: macro $i{name} to instead resolve to the objects getSaveData in a seperate macro, however with no success. I couldn't really understand how $i was getting the method named.

Then I came to a realisation that if getSaveData is generated by a macro, and getSaveData refers to other objects at run-time, perhaps when it builds, it will refer to a method that another macro hasn't yet generated. Not sure if this will be a problem, because the actual haxe calls of getSaveData will only happen at runtime. Just wondering.

Thanks a huge amount for your help : )
-Mix.


On Thursday, December 17, 2015 at 12:58:17 AM UTC+11, Haxiomic wrote:
Hey mixer, that's quite a good idea for a macro.

Snip.

Alexander Kuzmenko

unread,
Dec 17, 2015, 4:43:10 AM12/17/15
to Haxe
Instead of using @:build you can create an interface with @:autoBuild. All classes which implement that interface will be automatically built with your macro.
Then in a macro check fields types. If that type implements your interface, then you need to generate `field.getSaveData()` call instead of field value.

четверг, 17 декабря 2015 г., 11:02:41 UTC+3 пользователь Mixer написал:

Mixer

unread,
Dec 17, 2015, 5:56:37 AM12/17/15
to Haxe
Thanks for that, still have a few questions, sorry, new to macros. Given a Field object, how can I get the return value of Field.getSaveData() and put it in an expression?

Also, when you say "Then in a macro check fields types. If that type implements your interface, then you need to generate `field.getSaveData()` call instead of field value." Can you explain what you mean further? Do you mean check to see if it already implements getSaveData, and if it doesn't then apply the macro?

What I meant was how can I make getSaveData call other objects getSaveData(), and putting the data returned in, rather than the whole value. Lets say I am serialising the 'world' class. I put @:save on the player object. However the player I am referring to is a haxeflixel sprite, it would include all data (animation data etc) which I don't need. I only need player data marked with @:save (and thus should be in the getSaveData method of the player).

How very confusing. I guess being new to macro's implementing my whole game serialisation with them might not be the best idea, I could manually code all the 'getSaveData' methods. I will try with this a  little more though, I feel like I am nearly there. Thank you all so very much for your help, I really do appreciate it :D

Juraj Kirchheim

unread,
Dec 17, 2015, 7:01:00 AM12/17/15
to haxe...@googlegroups.com
The best code is no code at all. As an extension of that, the best macro is no macro at all. Because we live in an imperfect world, we have to compromise and write the simplest code (and macro in particular) possible.

When writing a macro, an important question to ask is how you would do it without, in a type safe manner, and then use the macro itself to reduce verbosity. In a few cases, the answer is that you simply can't, but those are hardly the best ones to tackle first ;)

Consider the following code: http://try.haxe.org/#04f18

What this accomplishes is to just leverage the type system to get the kind of recursion you want. Try removing any of the `implements SaveableObject`-clauses and you will be alerted that you're trying to save something probably not saveable. Awesome sauce!

Now all you need to do, is to add an `@:autoBuild`-macro to `SaveableObject`, that will generate the `getSaveData`-method if not present. That way you have a simple macro, that generates simple and robust code. And on top of that users can opt-out by providing their own implementation.

Best,
Juraj

--
To post to this group haxe...@googlegroups.com
http://groups.google.com/group/haxelang?hl=en
---
You received this message because you are subscribed to the Google Groups "Haxe" group.
For more options, visit https://groups.google.com/d/optout.

Haxiomic

unread,
Dec 17, 2015, 12:56:11 PM12/17/15
to Haxe
I agree with back2dos on doing things with haxe's regular typing system where possible is generally the best approach. Solutions like that tend to be more robust!

To continue with the macro version (partly because it's a good way to learn about macros) i've implemented Alexander Kuzmenko's idea with some new comments. There's not an awful lot more code involved but there's quite a bit more explanation!

The most important thing to remember about macros is you're composing haxe code objects, this is pretty much just the same as composing a string of haxe code, except rather than dealing with the raw strings (messy) you deal with objects that are converted into strings.

Here's an example of some code being parsed into code objects (a 'syntax tree') and then back into code in the browser console http://haxiomic.github.io/haxe-glsl-parser/

To turn a string of haxe code into code objects you use the macro keyword. So

macro var x = "hello world";

is the same idea as

parseHaxe('var x = "hello world"');

(parseHaxe doesn't exist as far as i'm aware)

The nice thing about macros in haxe however, is that they're executed during compilation so you've got access to lots of useful information about your program (like type definitions etc) which you can use to help compose your output.

This information can be accessed through the haxe.macro.Context object, so look up the api to get a flavour of what's available. (Be aware, not everything there is available to build macros, some only works for compiler macros but that's a different story)

In the new version of the macro, we use Context to look up information about types to see if they implement 'Savable', or if they are a class that subclasses something that implements Savable.


There's a demo usage at the bottom: this time to use the macro we add "implements Savable" to our class

Best,
George

Alexander Kuzmenko

unread,
Dec 17, 2015, 1:58:41 PM12/17/15
to Haxe
(parseHaxe doesn't exist as far as i'm aware)

Haxiomic

unread,
Dec 17, 2015, 2:03:11 PM12/17/15
to Haxe
Neat, I missed that one. Thanks, I suspect that'll come in handy some day.

Mixer

unread,
Dec 17, 2015, 5:31:54 PM12/17/15
to Haxe
You, my friend, are a legend. This is exactly what I was after, and comes with the bonus of if the macro ever gives me specific trouble, I can just put in my own getSaveData.

More importantly than that though, I have learnt a hell of a lot from this, and seeing how you adapted it to the problems I posted. Thank you heaps! I ow you one, not that I have anything you need :D

Cheers!
Reply all
Reply to author
Forward
0 new messages