5

I'm basically trying to create a macro that automatically generates an if/else if chain by supplying one common outcome statement for all conditions.

This is what I've tried so far (modified the code just to show as an example):

import haxe.macro.Expr;

class LazyUtils {

    public macro static function tryUntilFalse( xBool:Expr, xConds:Array<Expr> ) {
        var con1, con2, con3, con4, con5;

        /*
         * Here's a switch to handle specific # of conditions, because for-loops
         * don't seem to be allowed here (at least in the ways I've tried so far).
         * 
         * If you know how to use for-loop for this, PLEASE do tell!
         */
        switch(xConds.length) {
            case 1: {
                con1 = conds[0];
                return macro {
                    if (!$con1) $xBool;
                }
            }
            case 2: {
                con1 = conds[0];
                con2 = conds[1];
                return macro {
                    if (!$con1) $xBool;
                    else if (!$con2) $xBool;
                }
            }
            case 3: {
                con1 = conds[0];
                con2 = conds[1];
                con3 = conds[2];
                return macro {
                    if (!$con1) $xBool;
                    else if (!$con2) $xBool;
                    else if (!$con3) $xBool;
                }
            }
            // ... so on and so forth
        }

        return macro { trace("Unhandled length of conditions :("); };
    }
}

Then, in theory it could be used like this:

class Main {
    static function main() {
        var isOK = true;
        LazyUtils.tryUntilFalse( isOK = false, [
            doSomething(),
            doSomethingElse(), //Returns false, so should stop here.
            doFinalThing()
        ]);
    }

    static function doSomething():Bool {
        // ???
        return true;
    }

    static function doSomethingElse():Bool {
        // ???
        return false;
    }

    static function doFinalThing():Bool {
        return true;
    }
}

Which should generate this condition tree:

if (!doSomething()) isOK = false;
else if (!doSomethingElse()) isOK = false;
else if (!doFinalThing()) isOK = false;

Alternatively, I suppose it could output this instead:

if(!doSomething() || !doSomethingElse() || !doFinalThing()) isOK = false;

Looking back at this now, true - it may not make much sense to write a whole macro to generate code that would be easier to type out in it's raw format.

But for the sake of learning about macros, does anyone know if multiple expressions can be passed in an Array<Expr> like I tried in the above code sample?

Gama11
  • 31,714
  • 9
  • 78
  • 100
chamberlainpi
  • 4,854
  • 8
  • 32
  • 63

1 Answers1

4

You probably couldn't get the xConds argument to behave like you expected because the final argument of an expression macro with the type Array<Expr> is implicitly a rest argument. That means you ended up with an array that contained a single EArrayDecl expression. This can be fixed by simply omitting the [].

Regarding generating the if-else-chain - let's take a look at EIf:

/**
    An `if(econd) eif` or `if(econd) eif else eelse` expression.
**/
EIf( econd : Expr, eif : Expr, eelse : Null<Expr> );

The chain can be thought of as a singly linked list - the eelse if the first EIf should reference the next EIf and so forth, until we stop with eelse = null for the last EIf. So we want to generate this for your example (pseudo-code):

EIf(doSomething(), isOk = false, /* else */
    EIf(doSomethingElse, isOk = false, /* else */
        EIf(doFinalThing(), isOk = false, null)
    )
)

Recursion works well for this.

Typically it's more convenient to work with reification than raw expressions like I do here, but I'm not sure the former is really possible when dynamically generating expressions like this.

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

class LazyUtils {

    public macro static function tryUntilFalse(setBool:Expr, conditions:Array<Expr>):Expr {
        return generateIfChain(setBool, conditions);
    }

    private static function generateIfChain(eif:Expr, conditions:Array<Expr>):Expr {
        // get the next condition
        var condition = conditions.shift();
        if (condition == null) {
            return null; // no more conditions
        }

        // recurse deeper to generate the next if
        var nextIf = generateIfChain(eif, conditions);
        return {
            expr: EIf(condition, eif, nextIf),
            pos: Context.currentPos()
        };
    }
}

And Main.hx (mostly unchanged):

class Main {

    static function main() {
        var isOK = true;
        LazyUtils.tryUntilFalse(isOK = false,
            !doSomething(),
            !doSomethingElse(), //Returns false, so should stop here.
            !doFinalThing()
        );
    }

    static function doSomething():Bool {
        trace("doSomething");
        return true;
    }

    static function doSomethingElse():Bool {
        trace("doSomethingElse");
        return false;
    }

    static function doFinalThing():Bool {
        trace("doFinalThing");
        return true;
    }
}

To keep things simple I inverted the function call arguments with ! at the call site instead of handling that in the macro.

You can use -D dump=pretty to generate AST dumps and check what code is being generated. Here's the result:

if ((! Main.doSomething()))isOK = false else if ((! Main.doSomethingElse()))isOK = false else if ((! Main.doFinalThing()))isOK = false;
Gama11
  • 31,714
  • 9
  • 78
  • 100
  • +1 Thanks so much for this. Glad you could contribute your knowledge regarding the `Array` being essentially a `rest` argument. I can't imagine where I'd begin to look to find this information (or how it would occur to me that's how it worked). Oh, and if you have any good reads / links to share, I'd like to take a look at it! :) – chamberlainpi Apr 01 '16 at 20:56
  • (by links - don't necessarly mean only regarding the `rest` argument thing. Any helpful resources or tutorials to get better with Haxe Macros in general will do). – chamberlainpi Apr 01 '16 at 20:57
  • This article has a nice collection of links at the end: https://notes.underscorediscovery.com/haxe-compile-time-macros/ – Gama11 Apr 03 '16 at 08:54
  • 1
    Btw, http://code.haxe.org/ was just launched, which has a nice amount of macro examples already. – Gama11 Apr 03 '16 at 18:56