COMMUNITY

Macro to generate function calls from TypeDef


(Aartzrc) #1

I have some data objects that emit events when data comes in, here the basic idea:

class DataClass {
    public function receiveData(data:Data) {
        resolve_text(data.text);
        resolve_type(data.type);
        resolve_visible(data.visible);
    }

    // resolve_ calls emit events
}

typedef Data = {
    text:String,
    type:Int,
    visible:Bool
}

As the Data typedef gets more complex the redundant resolve_xx calls can get tedious to maintain. Is there a macro out there that can take the Data typedef and push out the resolve_xx functions and call them?

Or maybe I’m crazy and there’s a better way to emit the data? Thanks in advance.


(Simon Krajewski) #2

A macro would work nicely for this, but there’s probably not an existing one for your exact problem. Is your intention to only generate the calls, or also the resolve_ functions themselves? For the former, you need a simple expression-macro that you call from within your receiveData class. For the latter, you could use a build macro to generate the functions, and maybe the calls as well.


(Aartzrc) #3

I think it would be possible to generate the resolve_ functions (or inline code) along with the typedef parsing. Right now I’m using generics and PosInfos to gather details at compile time to partially create a generic resolve method. Promhx is the backing stream/promise library.

Here’s an addition to the above sample code that is my current resolve helper:

// See code in original post, the resolve_text(data.text) calls to here:
// resolve_text function is not really needed - it is just a shim to allow the compiler to detect the value type and PosInfos macro to give me the value name
function resolve_text(val:String) {
    _resolveVal(val); // _resolveVal is used by all resolve_ calls
}

// _resolveVal determines type and field name using generics and PosInfos
function _resolveVal<T>(val:T, force:Bool = false, ?pos:haxe.PosInfos) {
    var valName = pos.methodName.substr(pos.methodName.indexOf("resolve_")+8); // Bad hack, function must be named resolve_xx
    // Check for duplicate resolve and ignore
    if(!force && dataCache.exists(valName) && val == dataCache[valName]) {
        //trace('rejected: $valName : $val');
        return;
    }
    // Save to the value cache
    dataCache.set(valName, val);
    var valData = _deferredVal(valName);
    valData.resolve(val);
}

function _deferredVal<T>(valName:String) {
    // Lazy create the promhx.Deferred instance
    var valData:Deferred<T>;
    if(data.exists(valName)) {
        valData = cast data[valName];
    } else {
        valData = new Deferred<T>();
        data[valName] = valData;
    }
    return valData;
}

The goal would be to send a typedef to a macro that would extract the field names and types and output inline code to replace the _resolveVal call. The _deferredVal lazy create call could still be used, or a build macro could create the deferred instances at compile time.

Thanks for any starting code you can provide, a template macro that loops on the fields of a typedef would get me going.


(Juraj Kirchheim) #4

Well, there actually happens to be a library that does this out of the box :wink:

Example:

class Data implements coconut.data.Model {
    @:observable var text:String;
    @:observable var type:Int;
    @:observable var visible:Bool;
    @:transition function receiveUpdate(delta) return @patch delta; 
}

Minor clarification: by virtue of type inference delta will be { ?text:String, ?type:Int, ?visible:Bool } and those fields that are defined on delta and have a value different from the current one will be updated and the backing observables (think RxJS with a bit less ceremony) fire.

A field called observables is automatically generated on the model that exposes each of the underlying observables. There’s also a simpler way to consume the data. To illustrate both I took the liberty of putting together a small example which can be viewed in action online.


#5

I think the intent was to take

class DataClass {
    public function receiveData(data:Data) {
    }
}

typedef Data = {
    text:String,
    type:Int,
    visible:Bool
}

And with a Build Macro create the equivalent of

class DataClass {
    public function receiveData(data:Data) {
        resolve_text(data.text);
        resolve_type(data.type);
        resolve_visible(data.visible);
    }
}

I tried accessing a typedef, from a build Macro, in order to create the resolve calls but I was unable to get anywhere


(Juraj Kirchheim) #6

To my understanding the intent was to take an anonymous object and dispatch its properties onto streams of some flavor (in this case from promhx). If so, there’s a solution at hand. If not, then I’ve misinterpreted the problem ^^


(Aartzrc) #7

Thanks for the sample of coconut, that’s fun stuff! Hopefully I can work some of those tools into my current and future projects. The layered/functional style of the stream data would work well and it’s similar to some of the tools that promhx provides.

Hoseyhosey is on the right track too, I’m trying to use a typedef to create a class structure and callbacks. What you’ve got so far is a class that uses its own fields to create the remaining structure.

What I’m hoping for is shared typedefs (not shared classes) that can be included in multiple projects, that will enforce type safety as I reuse data. I update the typedef and custom classes are generated based on the target. This way I can have a repository of definitions, and auto build server and multiple clients based on JUST the typedef. I considered managing this by external files (json format file that is read in during compile to output macro expressions), but it seems better to handle it entirely in Haxe.

I found some time to play with macros and I’m pretty baffled that I can’t pull anonymous type fields. Keep in mind I’m a total hack at this, but here’s where I started digging:

using Type;
using Reflect;

import haxe.EnumTools;
import haxe.macro.Expr;

class Main2 {
    static function main() {
        var x = buildFromTypedef(DataDef);

        trace("fields outside of a macro:");
        var t:DataDef = { text: "blah", flag: true };
        for( f in Reflect.fields(t) ) {
			trace(f);
		}
    }

    static macro public function buildFromTypedef(td:ExprOf<TypeDefinition>) {
        trace("try to determine fields inside a macro:");
        trace(EnumValueTools.getParameters(td.expr)[0]);
        var cident = EnumValueTools.getParameters(td.expr)[0];
        trace(EnumValueTools.getParameters(cident)[0]);
        var typedefname = EnumValueTools.getParameters(cident)[0]; // Get DataDef string, but we need the DataDef typedef!
        trace(Type.getClass(typedefname)); // Yep, just a string

        // Maybe create an empty typedef based on typedefname and then iterate over fields?

        return macro { 1; };
    }
}

typedef DataDef = {
    text:String,
    flag:Bool
}

I feel like I’m going about this wrong, but it seems possible! Worst case I can parse as a text file and extract the fields that way. Thanks for any help!


(Aartzrc) #8

Thanks go to @ianharrigan of HaxeUI for some great tips on drilling down from the ExprOf<TypeDefinition> starting point! The final code is looking like this so far:

@:build(Macros.build(DataDef))
class Main2 {
    public static function main() {
    }
}

typedef DataDef = {
    text:String,
    type:Int,
    visible:Bool
}
import haxe.macro.Context;
import haxe.EnumTools;
import haxe.macro.Expr;
import haxe.macro.Expr.Field;
import haxe.macro.Type;

class Macros {
    macro public static function build(td:ExprOf<TypeDefinition>):Array<Field> {
        var typedefname:String = EnumValueTools.getParameters(EnumValueTools.getParameters(td.expr)[0])[0]; 
        var realtype = Context.getType(typedefname); // lets get the real type

        var ttype:Ref<DefType> = EnumValueTools.getParameters(realtype)[0];
        var anontype:Ref<AnonType> = EnumValueTools.getParameters(ttype.get().type)[0];
        for (tdf in anontype.get().fields) {
            trace('generate function: resolve_${tdf.name};');
        }
        
        return null;
    }
}

The trick that kicked it over was using the build macro Context.getType() function to pull a Ref<AnonType> which allows field iteration.

Does anyone know how to get the Ref<AnonType> directly from the ExprOf<TypeDefinition> ? Converting the type name to string and back seems like an unnecessary step but I haven’t found a better solution.


(Simon Krajewski) #9

The conversion is fair enough because you have to go from expression to type somehow. There are multiple avenues here, and going through Context.getType is fine.

What isn’t fine is that EnumValuesTools usage. Don’t use that, use normal switches. You can simplify your code like so:

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

using haxe.macro.Tools;

class Macros {
    macro public static function build(td:Expr):Array<Field> {
        var t = Context.getType(td.toString()).follow();
		var anon = switch (t) {
			case TAnonymous(ref): ref.get();
			case _: Context.error("Structure expected", td.pos);
		}
        for (tdf in anon.fields) {
            trace('generate function: resolve_${tdf.name};');
        }

        return null;
    }
}

(Aartzrc) #10

Fantastic, thanks! I was using EnumValuesTools to tighten things up, but I understand why that’s a bad idea considering the Array<Dynamic> return type looses type safety.

I’ve struggled with ways of drilling through Haxe enums without using switch, but it appears to be impossible? I would guess this is so the exhaustiveness check is guaranteed. In the case of this macro I know the incoming expression will be a typedef because I will be doing td:ExprOf<TypeDefinition>, so the case _ will never get called.

A note to other macro initiates: using haxe.macro.Tools provides the toString() / follow() and other methods that will give access to underlying data quickly. Without the using the code completion server doesn’t show you everything available. I know it’s in many examples, but it wasn’t obvious to me at first.