Search code examples
c#delegatesexpressionexpression-trees

How to combine Expression<TDelegate> objects without invoking?


I have a delegate as follows:

public delegate TestResult TestCase(byte[] source);

...where the return TestResult is as follows:

public class TestResult {

    public bool Pass { get; }
    public int Index { get; }

    public TestResult(bool result, int index) {
        Pass = result;
        Index = index;
    }
}

An example TestCase delegate looks like:

public static TestResult In(byte[] tkn, ITestSet testSet) {
    return testSet.Contains(tkn);
}

ITestSet is not much more than an encapsulated HashSet<byte[]>.

In one use case I have 2 test sets: (1) A - z, (2) 0 - 9. I want to test if an input byte[] is in either test set.

I am using Expression<TestCase> but having trouble figuring out how to implement the Or test case. I have a TestCaseBuilder with the following methods:

public class TestCaseBuilder {
    private Expression<TestCase> tcExpr;       

    public TestCaseBuilder With(TestCaseBuilder tcBuilder) {
        tcExpr = tcBuilder.tcExpr;
        return this;
    }

    public TestCaseBuilder Or(TestCaseBuilder tcBuilder) {
        tcExpr = tcExpr.Or(tcBuilder.tcExpr);        
        return this;
    }
}

...and my extension method:

public static Expression<TestCase> Or (this Expression<TestCase> tcLeft, Expression<TestCase> tcRight) {
    var lExpr = (LambdaExpression)tcLeft;
    var rExpr = (LambdaExpression)tcRight;    
    var param = lExpr.Parameters;

    return Expression.Lambda<TestCase>(/* what to do here ? */, param);
}

Expression.OrElse is mechanically what I would think is appropriate but cannot use that since I am returning a TestResult, not a bool.

Here is how the TestCaseBuilder is used:

testcaseBuilder.As("Foo")
    .With(isLetterTestCase)
    .Or(isDigitTestCase);

I have performed the Or using just the TestCase delegates:

public static TestCase Or(this TestCase tc1, TestCase tc2) {
    return tkn => {
        var res = tc1(tkn);
        if (res.Pass) {
            return res;
        }

        return tc2(tkn);
    };
}

How can I combine the 2 Expression<TestCase> in a custom Or method without invoking the first test case?


Solution

  • Is this what you wanted

            public static Expression<Func<byte[], TestResult, TestCase, TestResult>> helperExp = (inp, res, next) => res.Pass ? next(inp) : res;
    
            public static Expression<TestCase> Or(Expression<TestCase> exp1, Expression<TestCase> exp2)
            {
                var param = exp1.Parameters;
    
                Expression<TestCase> or = Expression.Lambda<TestCase>(
                   Expression.Invoke(helperExp,
                    param[0], Expression.Invoke(exp1, param), exp2),param);
    
                return or;
            }
    

    With a block expression no invoke

    public static Expression<TestCase> Or(Expression<TestCase> exp1, Expression<TestCase> exp2)
            {
                var param = exp1.Parameters;
    
                ParameterExpression local = Expression.Parameter(typeof(TestResult), "local");
    
                BlockExpression block = Expression.Block(
                    new[] { local },
                    Expression.Assign(local, exp1.Body),
                    Expression.Condition(Expression.Property(local, nameof(TestResult.Pass)), exp2.Body, local));
    
                return Expression.Lambda<TestCase>(block, param);
            }
    

    Test

                Expression<TestCase> exp1 = (tc) => new TestResult(true);
                Expression<TestCase> exp2 = (tc) => new TestResult(false);
    
                var first = Or(exp1, exp1);
    
                var second = Or(first, exp2);
    
                var func = second.Compile();
    
                var result = func(new byte[] { });
    

    There might be a better way to do a conditional monad without expressions using https://github.com/louthy/csharp-monad I thing .net core uses the monad principle for middleware.