Package com.singularsys.jep.misc.overloadedfunctions


package com.singularsys.jep.misc.overloadedfunctions
Classes that allow functions and operators to be overloaded adding new functionality. For each class two functions/operators and a predicate are specified. The actual function/operator used is chosen either during parsing or during evaluation depending on the setup.

Evaluation time overloading

The OverloadedUnaryFunction, OverloadedBinaryFunction, and OverloadedNaryFunction allows functions to be chosen depending on the evaluation time values of the arguments. These classes allow the functions and operators to be overloaded to work with new types, without modifying the original function. For example if you have a class MyVal which you want to use

class MyVal {
    double value;

    public MyVal(double value) { this.value = value; }

    public MyVal add(MyVal obj) {
        return new MyVal(value + obj.value);
    }
    public MyVal sub(MyVal obj) {
        return new MyVal(value - obj.value);
    }
    public MyVal neg() {
        return new MyVal(-value);
    }
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof MyVal) {
            MyVal obj2 = (MyVal) obj;
            return value == obj2.value;
        }
        return false;
    }
}

You can create overloaded functions for the operators +, -, and unary minus using the following code:

OverloadedUnaryFunction uminus = new OverloadedUnaryFunction(
    UnaryFunction.instanceOf(MyVal.class,x->x.neg()),
    new UMinus(),
    x -> x instanceof MyVal);
OverloadedBinaryFunction sub = new OverloadedBinaryFunction(
    BinaryFunction.instanceOf(MyVal.class,(x,y)->x.sub(y)),
    new Subtract(),
    (x,y) -> x instanceof MyVal && y instanceof MyVal);
OverloadedNaryFunction add = new OverloadedNaryFunction(
    NaryBinaryFunction.instanceOf(MyVal.class,(x,y)->x.add(y)),
    new Add(),
    args -> Stream.of(args).allMatch(arg -> arg instanceof MyVal));
jep.getOperatorTable().getUMinus().setPFMC(uminus);
jep.getOperatorTable().getAdd().setPFMC(add);
jep.getOperatorTable().getSubtract().setPFMC(sub);
jep.reinitializeComponents();
In each constructor the first argument is a PFMC that works with MyVal objects, the second argument is the regular operator, and the third argument is a predicate that determines if the function should be used for a given set of arguments. Static factory methods are used to create the PFMCs from lambda expressions.

With this setup you can now use the operators +, -, and unary minus with MyVal objects as well as regular numbers.

Node addnode = jep.parse("x+y");
Node subnode = jep.parse("x-y");
Node negnode = jep.parse("-x");

// Evaluating with MyVal objects
jep.setVariableValue("x", new MyVal(3.0));
jep.setVariableValue("y", new MyVal(4.0));
Object res = jep.evaluate(addnode);
Object res2 = jep.evaluate(subnode);
Object res3 = jep.evaluate(negnode);
assertEquals(new MyVal(7.0),res);
assertEquals(new MyVal(-1.0),res2);
assertEquals(new MyVal(-3.0),res3);

// Evaluating with regular numbers
jep.setVariableValue("x", 3.0);
jep.setVariableValue("y", 4.0);
Object res4 = jep.evaluate(addnode);
Object res5 = jep.evaluate(subnode);
Object res6 = jep.evaluate(negnode);
assertEquals(7.0,res4);
assertEquals(-1.,res5);
assertEquals(-3.,res6);

Parse time overloading

A set of alternate functions can be chosen at parse time based on the number of arguments by using the AlternateFunctions class. For example you might want to overload the atan function so that it could have one argument and two arguments versions.

UnaryFunction uf = new ArcTangent();
BinaryFunction bf =new ArcTangent2();
AlternateFunctions af = new AlternateFunctions(uf, bf);
jep.addFunction("atan",af);
jep.reinitializeComponents();
{
   Node node = jep.parse("atan(1)");
   Object res = jep.evaluate(node);
   assertEquals(Math.PI/4,(double) res,1e-9);
}
{
   Node node = jep.parse("atan(1,2)");
   Object res = jep.evaluate(node);
   assertEquals(Math.atan2(1,2),(double) res,1e-9);
}

With a standard setup the functions are chosen during evaluation. However, by using the AlternateFunctionGrammerMatcher class the ConfigurableParser can be modified to substitute the functions when it is parsed.

To setup the parser to use the AlternateFunctionGrammerMatcher you could use the StandardConfigurableParser and replace the default FunctionGrammarMatcher.

var cp = new StandardConfigurableParser();
cp.replaceGrammarMatcher(FunctionGrammarMatcher.class,
    new AlternateFunctionGrammerMatcher(
        new FunctionGrammarMatcher(
            cp.getSymbolToken("("),
            cp.getSymbolToken(")"),
            cp.getSymbolToken(","))));
jep = new Jep(cp);

With this setup the function is replaced with the correct PFMC during parsing:

UnaryFunction uf = new ArcTangent();
BinaryFunction bf =new ArcTangent2();
AlternateFunctions af = new AlternateFunctions(uf, bf);
jep.addFunction("atan",af);
jep.reinitializeComponents();

Node node = jep.parse("atan(1)");
assertSame(uf,node.getPFMC());
Node node2 = jep.parse("atan(1,2)");
assertSame(bf,node2.getPFMC());

A further class OverloadedFunctionByStructure allows overloaded functions chosen by the structure of parse tree. With the same setup as above the functions are also resolved by the AlternateFunctionGrammerMatcher.

UnaryFunction uf = new ArcTangent();
BinaryFunction bf =new ArcTangent2();
var af = new OverloadedFunctionByStructure(uf, bf, n->n.jjtGetNumChildren()==1);
jep.addFunction("atan",af);
jep.reinitializeComponents();

Node node = jep.parse("atan(1)");
assertSame(uf,node.getPFMC());
Node node2 = jep.parse("atan(1,2)");
assertSame(bf,node2.getPFMC());

OverloadedOperators and the OverloadResolver

The ConfigurableParser can not resolve operator is defined using an OverloadedFunctionByStructure to the correct pfmc. Here the OverloadResolver visitor can be used to resolve operators and functions after parsing and before evaluation.

We might wish to extend the list operator, [ ] so it can work with two different syntax:

  • [1,2,3] using the standard List operator and function
  • [1..5] representing a sequence of numbers
To allow for this syntax, first define a dot-dot operator, using the with a Range PostfixMathCommand, which returns a list of values between the endpoints.
Operator dotdotOp = new Operator("..",new Range(),Operator.BINARY);
ot.appendOperator(dotdotOp, ot.getAssign());
The OverloadedOperator.extendOperator() static factory create an OverloadedOperator. This adds three operators to the OperatorTable:
  • A overloaded operator, used during parse time
  • An alternate operator, with the Range pfmc
  • The original list operator, modified so its not used at parse time
and a predicate testing the child of the node n -> n.jjtGetChild(0).getOperator()==dotdotOp.
var overOp = OverloadedOperator.extendOperator(
    jep,
    new Identity(),
    ot.getList(),
    n -> n.jjtGetChild(0).getOperator()==dotdotOp);
The alternate operator and modified list operator can be retreived using the OverloadedOperator.getOp1() and OverloadedOperator.getOp2() methods.
var sequenceOp = overOp.getOp1();
var listOp = overOp.getOp2();
During parsing the overOp will be used.
Node node = jep.parse("[1..3]");
assertSame(overOp,node.getOperator());
This can be evaluated correctly. However it is more efficient to resolve the correct operator before evaluations using the OverloadResolver.
Node node1 = jep.parse("[1..3]");
or.visit(node1);
assertSame(sequenceOp,node1.getOperator());
Object res = jep.evaluate(node1);

Node node2 = jep.parse("[1,3]");
or.visit(node2);
Object res2 = jep.evaluate(node2);
assertSame(listOp,node2.getOperator());
The extendOperator factory method, performs all the necessary initilisations, it adds all three operators to the operator table, and sets the flags and precidences. If the original operator, has alternate symbols for parsing those are copied to the other operators, likewise if it has a print symbol thats is also copied, and if has an associated PrintRuleI in the PrintVisitor that rule is used for the other two operators.

The OverloadedOperator can also be constructed manually:

Operator dotdotOp = new Operator("..",new Range(),Operator.BINARY);
ot.appendOperator(dotdotOp, ot.getAssign());

listOp = ot.getList();
int flags = listOp.getFlags();
// Ensure the orginal operator is not used by the parser
listOp.setFlag(Operator.NOT_IN_PARSER, true);
int flags2 = flags | Operator.NOT_IN_PARSER;
// Construct the alternate operator
var idFun = new Indentity();
sequenceOp = new Operator(
    "LIST:alt",
    listOp.getSymbol(),
    idFun,
    flags2);
// Construct the overloaded operator
overOp = new OverloadedOperator(
    "LIST:overloaded",
    listOp.getSymbol(),
    sequenceOp,
    listOp,
    n -> n.jjtGetChild(0).getOperator()==dotdotOp,
    flags,
    listOp.getPrecedence());
// Adds all three operators to the table.
// Using replaceOperator ensures this operator is used the the
// Operator tables getList() method.
ot.replaceOperator(listOp, overOp);
ot.addOperator(listOp, overOp);
ot.addOperator(sequenceOp, overOp);
jep.reinitializeComponents();
// Ensure correct rules are used for printing
var rule = jep.getPrintVisitor().getSpecialRule(listOp);
if(rule!=null) {
    jep.getPrintVisitor().addSpecialRule(overOp, rule);
    jep.getPrintVisitor().addSpecialRule(sequenceOp, rule);
}
var printSym = listOp.getPrintSymbol();
if(printSym!=null) {
	   overOp.setPrintSymbol(printSym);
	   sequenceOp.setPrintSymbol(printSym);
}
// Copy any alternate symbols used for parsing
var altSym = listOp.getAltSymbols();
if(altSym != null) {
    for(var sym:altSym) {
        overOp.addAltSymbol(sym);
        sequenceOp.addAltSymbol(sym);
    }
}
An alternative approach would be to use a single Operator that uses a OverloadedFunctionByStructure
Operator dotdotOp = new Operator("..",new Range(),Operator.BINARY);
ot.appendOperator(dotdotOp, ot.getAssign());

var listOp = ot.getList();
var idFun = new Identity();
var listFun = listOp.getPFMC();
var overFun = new OverloadedFunctionByStructure(
    idFun,
    listFun,
    n -> n.jjtGetChild(0).getOperator()==dotdotOp
    );
listOp.setPFMC(overFun);
jep.reinitializeComponents();
The disadvantage of this method is that it brakes the assumptions that node.getPFMC() == node.getOperator().getPFMC(). Using the OverloadedOperator maintains that assumption. This affects visitors like the SubstitutionVisitor which builds nodes using the Operator PFMC. It also affects MacroFunction which uses these visitor internally.
Since:
Jep 4.1
See Also: