Groovy Science Expression Validation (Part 2)

Finally! Instead of writing this entry a week ago like I said I would, I kept pushing it aside in favor of pushing ahead on the project.

So, what kinds of things can CumulativeExpressionValidator do? Well, the goal was to be able to declare an allowed SymbolicExpression operator-argument combination in a way similar to declaring a function. In other words, my goal was to let people do something like this:

numberContext.allowAlso( plusOp, [ numberContext, numberContext ] );

The numberContext expression validator presumably asks “Does the expression ultimately result in a number?” and this statement should modify that validator to also think, “If it’s ultimately adding two numbers, then yes, it does ultimately result in a number.”

Expression validators are very much intended to be used together. For instance, I immediately imagined them being used in the following way:

numberContext.allowAlso( plusOp, [ numberContext, numberContext ] );
booleanContext.allowAlso( equalsOp, [ numberContext, numberContext ] );
booleanContext.allowAlso( orOp, [ booleanContext, booleanContext ] );
numberContext.allowAlso( ifThenElseOp, [ booleanContext, numberContext, numberContext ] );

I went a little bit crazy and overloaded the methods in the name of convenience. I think actions might speak louder than words here:

numberContext.allowAlso( [ plusOp, timesOp ], { it.every { it in numberContext } } );
numberContext.allowAlso( [ minusOp, dividedByOp ], [ numberContext, numberContext ] );
numberContext.allowOnly( dividedByOp, [ { true }, { !isObviouslyZero( it ) } ] );

Essentially, there are three ways to use allowAlso:

  • Pass a single Object expressionCase. For any expression e, if (e in expressionCase), e will be allowed.
  • Pass two Objects, operatorCase and argumentListCase. For any expression e, if (e.operator in operatorCase), then if (e.argumentList in argumentListCase), e will be allowed.
  • Pass an Object operatorCase and a List< Object > argumentCases. For any expression e, if (e.operator in operatorCase) and e‘s arguments match up one-to-one with the argumentCases, then if each argument index i has (e.argumentList[ i ] in argumentCases[ i ]), e will be allowed. For example, if the arguments are c1 and [ c2, c3 ], then any expression with ((e.operator in c1) && (e.argumentList.size() == 2)) and ((e.argumentList[ 0 ] in c2) && (e.argumentList[ 1 ] in c3)) will be allowed.

Conveniently, CumulativeExpressionValidator itself implements the isCase method, which means that a validator can be used as an argument case as seen above. Just as conveniently (if not more so), Closures like the ones in my examples also work. If something doesn’t work, you can always write a Closure that works the way you expect.

The allowOnly method, which I gave you an untimely peek of, is used essentially the same way as the allowAlso method, but it imposes restrictions instead. These are the three ways to use it (with the differences emphasized):

  • Pass a single Object expressionCase. For any expression e, unless (e in expressionCase), e will be forbidden.
  • Pass two Objects, operatorCase and argumentListCase. For any expression e, if (e.operator in operatorCase), then unless (e.argumentList in argumentListCase), e will be forbidden.
  • Pass an Object operatorCase and a List< Object > argumentCases. For any expression e, if (e.operator in operatorCase) and e‘s arguments match up one-to-one with the argumentCases, then unless each argument index i has (e.argumentList[ i ] in argumentCases[ i ]), e will be forbidden. For example, if the arguments are c1 and [ c2, c3 ], then any expression with ((e.operator in c1) && (e.argumentList.size() == 2)) but not ((e.argumentList[ 0 ] in c2) && (e.argumentList[ 1 ] in c3)) will be forbidden.

In short, allowAlso acts as an “or,” allowing the newly specified case in addition to the old cases, whereas allowOnly acts as an “and,” allowing only those old cases which also satisfy the newly specified one.

The sneaky thing to keep track of (as if there’s only one) is that if there’s an operator case involved, then that is interpreted as a condition that must be met for the filter to apply. To repeat a previous example, this kind of statement was the motivation for allowOnly:

numberContext.allowOnly( dividedByOp, [ { true }, { !isObviouslyZero( it ) } ] );

If allowOnly forced all candidate expressions to also satisfy this new operator case, then dividedByOp would become the only legal operator! Since I don’t want that, I’m much happier treating the operator case as a condition rather than as part of the restriction.

Now, I’ve already hinted that this method overloading was a crazy idea, and now I’ll say why: It’s because the overloaded methods take big steps but don’t go all the way. The operator case is used as a condition, but there’s no way to make a condition that hinges on one of the arguments (rather than the operator). Technically, that kind of thing will never be needed when using allowAlso, and the one-argument version of allowOnly is perfectly capable of handling it if the user wants to write the necessary code him- or herself, but it feels to me like there’s something promised that wasn’t followed through on.

So, there’s room for improvement. Who knew? Even though there are shortcomings, I’m more than happy to provide even a subset of the functionality I’ve implemented, so I feel ready to move on.

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