I have a bit of a unique situation. For various reasons, chiefly interoperating with a nullable stringly typed legacy system, as well as various other needs I won't go into at the moment, I've settled on a custom @:enum abstract that looks like this:
@:enum abstract MyEnum(Null<Int>) {
public var A = 0;
public var B = 1;
public var C = 2;
public var D = 3;
public var E = 4;
public var F = 5;
public var G = 6;
@:from private static function fromString (value:String):MyEnum {
return switch (value) {
case "a": A;
case "b": B;
case "c": C;
case "d": D;
case "e": E;
case "f": F;
case "g": G;
default: null;
}
}
@:to private static function toString (value:Int):String {
return switch (value) {
case A: "a";
case B: "b";
case C: "c";
case D: "d";
case E: "e";
case F: "f";
case G: "g";
default: null;
}
}
}
However, that's an annoyingly large amount of things to type, and when adding and removing members it's easy to make a manual error. Clearly, this follows a super predictable pattern and seems like a great thing to construct with a macro, but I am terrible at haxe macros.
Can someone explain how I could use a macro to build this enum in such a way that all I have to supply is a list of field names?
pseudocode:
@:enum abstract MyEnum = doTheMacroMagic(["A","B","C","D","E","F","G"]);
The logical steps would be:
I think a simple practical example like this might make haxe macros finally "click" for me if I can see it in action.
Flixel handles a very similar use case in for classes like FlxKey
with FlxMacroUtil.buildMap()
. This expression macro looks for all uppercase, inline vars it finds in the abstract and generates a Map<String, EnumType>
from it, with the keys being the field names and the values the field values (or the inverse of that if invert
is true
).
@:enum
abstract FlxKey(Int) from Int to Int
{
public static var fromStringMap(default, null):Map<String, FlxKey>
= FlxMacroUtil.buildMap("flixel.input.keyboard.FlxKey");
public static var toStringMap(default, null):Map<FlxKey, String>
= FlxMacroUtil.buildMap("flixel.input.keyboard.FlxKey", true);
var A = 65;
var B = 66;
// more keys...
@:from
public static inline function fromString(s:String)
{
s = s.toUpperCase();
return fromStringMap.exists(s) ? fromStringMap.get(s) : NONE;
}
@:to
public inline function toString():String
{
return toStringMap.get(this);
}
}
I'd imagine that's a good starting point. If you want to generate the entire abstract
, you will need a @:build
macro.
Answering the follow-up question, how to generate fields: This is actually quite straightforward with a build macro:
@:enum
@:build(Macro.createVariables(["A", "B", "C", "D", "E"]))
abstract Generated(Int)
{
}
Macro.hx
(sensible to have in its own file to avoid having to deal with #if macro
conditionals):
package;
import haxe.macro.Context;
import haxe.macro.Expr;
class Macro
{
public static macro function createVariables(varNames:Array<String>):Array<Field>
{
// get the current fields of the calling type (empty array in this case)
var fields = Context.getBuildFields();
for (i in 0...varNames.length)
// create a custom variable and add it to the fields
fields.push(createVariable(varNames[i], i));
return fields;
}
private static function createVariable(name:String, value:Int):Field
{
return {
name: name,
doc: null,
meta: [],
access: [Access.APublic, Access.AStatic, Access.AInline],
kind: FieldType.FVar(macro:Int, macro $v{value}),
pos: Context.currentPos()
}
}
}
You'll notice the fields showing up in auto-completion for Generated.
. You can also see what's been generated by looking at the Generated_Impl.dump
when doing an AST dump.