Groovy Science Expression Evaluation

So, up to this point, there have been ways to create expressions and ways to filter out the incorrect ones, but in the past week, I’ve implemented a couple of ways to actually do things with them. I just finished talking about CumulativeExpressionValidator, so now I present to you CumulativeExpressionEvaluator. As with CumulativeExpressionValidator, I’m not so sure that the interface is very good, but I’ll show you what I have.

If CumulativeExpressionValidator attempts to make validator creation feel like function declaration, then CumulativeExpressionEvaluator attempts to make evaluator creation feel like function definition. I know, I know, if it’s so much like normal programming, why not just define your own Groovy functions and use them instead? Well, you could, and it’s actually not at all difficult for simpler evaluators, but the goal of CumulativeExpressionEvaluator is to help specify the behavior of an evaluator object one piece at a time so that it can be passed around different parts of a program and have features added to it and removed from it over time. If you have to whip that up on the spot, you’ll probably want to put it in its own class at some point, and you’ll probably call that class something like… oh, “CumulativeExpressionEvaluator.”

Anyway, once I was ready to move on from CumulativeExpressionValidator, I realized that if that class could let its “validation” result in something other than a boolean true or false, then it could work as an evaluator. So I’ve essentially reworked its interface for that purpose. Here’s an example:

import static org.codehaus.groovy.science.ConstantOperator.*
import static org.codehaus.groovy.science.CumulativeExpressionEvaluator.*

def numericConstantFolder = new CumulativeExpressionEvaluator();

numericConstantFolder.setBehavior( filter( ConstantOperator ), [], { it } );

numericConstantFolder.setBehavior(
    filter( OverloadableOperators.Plus ),
    [ numericConstantFolder, numericConstantFolder ],
    withArgs( inCon { a, b -> a + b } )
);

Here’s the same example without the helper functions:

def numericConstantFolder = new CumulativeExpressionEvaluator();

numericConstantFolder.setBehavior(
    { it in ConstantOperator ? it : null },
    [],
    { it }
);

numericConstantFolder.setBehavior(
    { it in OverloadableOperators.Plus ? it : null },
    [ numericConstantFolder, numericConstantFolder ],
    { new SymbolicExpression( new ConstantOperator(
        it.argumentList[ 0 ].operator.value + it.argumentList[ 1 ].operator.value
    ), [] ) }
);

This evaluator will transform any SymbolicExpression that is a sum of constant values into another SymbolicExpression that is a single constant value representing the calculated sum. Presenting it with any expression more complicated than that (for instance, an expression that uses an operator other than ConstantOperator or Plus) will result in null.

Static helper methods in ConstantOperator help in the creation and manipulation of constant values. By statically importing the helper methods con and unCon, we can write the expression unCon( numericConstantFolder( con( 1 ) + con( 2 ) ) ) and expect to get the value 3. The method inCon, as used in the example above, changes something like { a, b -> a + b } into something like { a, b -> con( unCon( a ) + unCon( b ) ) }. It is used to make any “normal” Groovy closure into a closure that “works inside” of SymbolicExpressions that represent constant values.

The first behavior given in the example determines whether the operator is a ConstantOperator and whether the argument list is empty, and if so, it simply returns a SymbolicExpression that is equivalent to the one it recieved. If the operator is something else, or if the argument list is not empty, no behavior is specified, and the default behavior (which, so far, is to return null) is used.

The second behavior determines whether the operator is OverloadableOperators.Plus (the operator that is used in the resulting SymbolicExpression when the Groovy operator + is applied to two SymbolicExpression objects), whether there are two arguments, and whether numericConstantFolder maps those arguments to non-null values, and if so, it returns a new SymbolicExpression that represents the constant value that results from adding the constant values represented by the SymbolicExpressions returned by numericConstantFolder for those arguments.

*takes a deep breath* I might have been better off starting with a simpler example….

I’ll try explaining this again with a different approach. If you already understand what I’m getting at, sorry for dumbing it down now; if you don’t, sorry for being so cryptic the first time through, and thanks for staying with me for this long!

Let’s say a “fixer” is a function that takes a single value and either returns the fixed value (if it can fix it) or null (if it can’t). A CumulativeExpressionEvaluator is a fixer for SymbolicExpressions, and its selling point is that it can be built up from other fixers, one at a time. Whenever a new fixer is added to the CumulativeExpressionEvaluator, it essentially overwrites all of the others, except when it returns null. Using just this idea, we can implement the above example as follows:

import static org.codehaus.groovy.science.ConstantOperator.con
import static org.codehaus.groovy.science.ConstantOperator.unCon

def numericConstantFolder = new CumulativeExpressionEvaluator();

numericConstantFolder.setBehavior( { expression -> (
    if ( !(
        (expression.operator in ConstantOperator)
        &&
        expression.argumentList.isEmpty()
    ) )
        return null;

    return expression;
) } );

numericConstantFolder.setBehavior( { expression ->
    if ( !(
        (expression.operator in OverloadableOperators.Plus)
        &&
        (expression.argumentList.size() == 2)
    ) )
        return null;

    def a = numericConstantFolder( expression.argumentList[ 0 ] );

    if ( a == null )
        return null;

    def b = numericConstantFolder( expression.argumentList[ 1 ] );

    if ( b == null )
        return null;

    return con( unCon( a ) + unCon( b ) );
} );

Specifying things this way is still perfectly possible, but notice that a full half of the second behavior deals just with getting the arguments into the shape we’d like them to be in. I’d much rather specify this recursion using something like [ numericConstantFolder, numericConstantFolder ], like we did with CumulativeExpressionValidator. Since that list is a list of fixers, I’ve overloaded setBehavior so that you can specify an operator fixer and a list of argument fixers to apply to an expression before applying the “actual” behavior. That lets us shorten the setBehavior calls to the following:

numericConstantFolder.setBehavior(
    { (it in ConstantOperator) ? it : null },
    [],
    { it }
);

numericConstantFolder.setBehavior(
    { (it in OverloadableOperators.Plus) ? it : null },
    [ numericConstantFolder, numericConstantFolder ],
    { expression ->
        con(
            unCon( expression.argumentList[ 0 ] )
            + unCon( expression.argumentList[ 1 ] )
        );
    }
} );

By statically importing CumulativeExpressionEvaluator.filter, we can specify simple fixers like { (it in ConstantOperator) ? it : null } using the form filter( ConstantOperator ). This leaves us with the following code:

numericConstantFolder.setBehavior( filter( ConstantOperator ), [], { it } );

numericConstantFolder.setBehavior(
    filter( OverloadableOperators.Plus ),
    [ numericConstantFolder, numericConstantFolder ],
    { expression ->
        con(
            unCon( expression.argumentList[ 0 ] )
            + unCon( expression.argumentList[ 1 ] )
        );
    }
);

(Personally, I don’t really like seeing the calls to filter there. For that reason, I’ve implemented setBehaviorCase methods that work essentialy the same as the setBehavior methods except that filter is called implicitly. That takes away the chance to use something like [ numericConstantFolder, numericConstantFolder ], though, so I’m still not happy with those, and I won’t bother using them here. I’ll probably end up redesigning the interface in the near future.)

The first setBehavior call is about as abstract as I expect it to get. In fact, it looks at least a little bit arcane without the more complicated setBehavior calls there to give the [] and { it } meaning. That said, that kind of thing happens all the time with empty brackets and null values in programming lanugages already, so I don’t think it’s an issue that this library needs to solve. If it’s a big deal for you, I suggest defining a well-named convenience method/closure or monkey patching a new method onto the CumulativeExpressionEvaluator meta class to help reduce the clutter. Or, you know, commenting.

In any case, we had to explicitly say “expression.argumentList[ 0 ]” and “expression.argumentList[ 1 ],” but that’s something we can abstract away. I expect that processing the arguments of the expression will be very common (and that calculations involving the operator will be somewhat more uncommon), so I’ve provided the utility method CumulativeExpressionEvaluator.withArgs that converts a closure into another closure that takes a SymbolicExpression and calls the original closure with the expression’s arguments. If we statically import the withArgs method, we can shorten the second setBehavior call to this:

numericConstantFolder.setBehavior(
    filter( OverloadableOperators.Plus ),
    [ numericConstantFolder, numericConstantFolder ],
    withArgs { a, b -> con( unCon( a ) + unCon( b ) };
} );

Using ConstantOperator.inCon, that simplifies to this:

numericConstantFolder.setBehavior(
    filter( OverloadableOperators.Plus ),
    [ numericConstantFolder, numericConstantFolder ],
    withArgs( inCon { a, b -> a + b } );
} );

And this is exactly the overcomplicated example I gave at the beginning.

Just so you know, there are more ways to call setBehavior that I purposefully neglected to go over until now. Just as you can provide a “preemptive” fixer for the operator and each of the arguments, you can instead provide just a fixer for the operator and a fixer for the argument list as a whole, or you can provide a single preemptive fixer for the entire expression. The last combination seems a bit silly to me—you fix the expression before you fix the expression—but it might turn out to be especially convenient when composing multiple existing CumulativeExpressionEvaluators.

Make sure to tune in next time. I hope to talk about the pattern search-and-replace functionality I’ve provided so far. Then again, I think I might push the blog aside again for a bit so that I have a more feature-complete pattern-matching system in place to show you. At this point, I can only match patterns that completely encompass their subject expressions, and I’d really rather match all applicable subexpressions one by one.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s