Haxe Code Cookbook
Haxe programming cookbookMacrosExtract values with pattern matching

Extract values with pattern matching

Reading time: 1 minute

Mostly useful to extract enum values from known enum instances. Allows to extract multiple variables at once. Takes most of expressions that are possible to use in switch expression.

Implementation

package;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
#end

class Match {
    
    public static macro function extract(value:Expr, pattern:Expr, ifNot:Array<Expr>) {
        var ifNot = switch(ifNot.length) {
            case 0: macro throw "don't match";
            case 1: ifNot[0];
            default: Context.error('too much arguments', ifNot[1].pos);
        }
        
        var params = [];
        function getParamNames(expr:ExprDef) {
            switch(expr) {
                case EConst(CIdent(name)) | EBinop(OpArrow, _, {expr:EConst(CIdent(name))}): if (name != '_' && params.indexOf(name) < 0) params.push(name);
                case ECall(_, params): for (param in params) getParamNames(param.expr);
                case EBinop(OpArrow, _, expr): getParamNames(expr.expr);
                case EBinop(OpOr, expr0, expr1): getParamNames(expr0.expr); getParamNames(expr1.expr);
                case EObjectDecl(fields): for (field in fields) getParamNames(field.expr.expr);
                case EArrayDecl(values): for (value in values) getParamNames(value.expr);
                case EParenthesis(expr): getParamNames(expr.expr);
                default:
            }
        }
        getParamNames(pattern.expr);
        
        var resultExpr = switch(params.length){
            case 1: macro $i{params[0]};
            case _: {expr:EObjectDecl(params.map(function(paramName) return {field:paramName, expr:macro $i{paramName}})), pos:Context.currentPos()};
        }
        
        return macro {
            switch($value) {
                case $pattern: $resultExpr;
                case _: $ifNot;
            }
        };
    }

    
}

Usage

package;

enum Tree<T> {
  Leaf(v:T);
  Node(l:Tree<T>, r:Tree<T>);
}

class MatchTest{

    static function assert(v, ?pos : haxe.PosInfos) if (!v) throw 'Assert failed. ' + pos;
    
    static function main() {
        //Enum matching
        assert("leaf0" == Match.extract(Leaf("leaf0"), Leaf(name)));
        assert("leaf1" == Match.extract(Node(Leaf("leaf0"), Leaf("leaf1")), Node(_, Leaf(leafName))));
        
        var result = Match.extract(Node(Leaf("leaf0"), Leaf("leaf1")), Node(Leaf(leafName0), Leaf(leafName1)));
        assert("leaf0" == result.leafName0);
        assert("leaf1" == result.leafName1);
        
        //Structure matching
        var myStructure = {
            name: "haxe",
            rating: "awesome"
        };
        assert("haxe" == Match.extract(myStructure, {name:name}));
        var result = Match.extract(myStructure, {name:n, rating:r});
        assert("haxe" == result.n);
        assert("awesome" == result.r);
        
        //Array matching
        var myArray = [1, 6];
        assert(6 == Match.extract(myArray, [1, a])); 
        
        //Or patterns
        assert(2 == Match.extract(Node(Node(null, null), Leaf(2)), (Node(Leaf(s), _)|Node(_, Leaf(s)))));
        
        //Guards - not supported due to haxe macro syntax limitations
        //var result = Match.extract(myArray, [a, b] if a < b);
        //assert(result.a == 1 && result.b == 6);
        
        //Match on multiple values
        //have to force type to Array<Dynamic> to make it work, looks ugly
        var result = Match.extract(([1, false, "foo"]:Array<Dynamic>), [a, b, c]);
        assert(result.a == 1 && result.b == false && result.c == "foo");
        
        //Extractors
        assert('foo' == Match.extract(Leaf('Foo'), Leaf(_.toLowerCase() => r)));
        
        //default value if not match
        assert(3 == Match.extract(myArray, [a, -1], 3));
        
        trace('ok');
    }
    
}

Contributors:
romamik
Last modified:
Created:
Category:  Macros