Expression evaluation using visitor pattern

笑着哭i 提交于 2020-05-28 04:51:52

问题


I have been trying to design an expression evaluator using visitor framework. Before going into my current design here are few problems that I am facing.

  1. Current design works well with arithmetic operators but when it comes to logical operators for eg 5 > 6 will output to false it doesn't work. As Visitor is a generic interface so if I say Visitor<Boolean> the return value will become boolean but participating values are integer hence I am not able to have integer as parameter.
  2. The design requires a class for every operator can it be minimized?
  3. Please share your inputs on design. Does it look good or any modification needed.

Following are participating classes.

Here is visitor interface

public interface Visitor<T> {

    T visit(Expression expression);

    T visit(BinaryExpression binaryExpression);

    T visit(ConstantExpression expression);
}

visitor implementation

public class ExpressionVisitor<T> implements Visitor<T> {

    @Override
    public T visit(Expression expression) {
        return expression.accept(this);
    }

    @Override
    public T visit(BinaryExpression arithmeticExpression) {
        Operator op = arithmeticExpression.getOperator();
        T left = visit(arithmeticExpression.getLeft());
        T right = visit(arithmeticExpression.getRight());
        return op.apply(left, right);
    }

    @Override
    public T visit(ConstantExpression expression) {
        return (T) expression.getValue();
    }
}

A class which represents expression

public abstract class Expression {

    public abstract  <T>  T accept(Visitor<T> visitor);
}

Class for binary expression

public class BinaryExpression extends Expression {

    private Operator operator;
    private Expression left;
    private Expression right;

  //constructor and getters

    @Override public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this);
    }
}

A class that represents Constant value

public class ConstantExpression extends Expression {


    private Object value;

//constructor and getter

    @Override public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this);
    }
}

Operator clas

public abstract class Operator {

    public <T> T apply(T left, T right) {
        try {
            Method method = this.getClass().getMethod("do" + left.getClass().getSimpleName() + this.getClass().getSimpleName(), left.getClass(), right.getClass());
            method.setAccessible(true);
            return (T) method.invoke(this, left, right);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
           throw new RuntimeException("Failed");
        }
    }
}

And Addition operator

public class Addition extends Operator {


    public Integer doIntegerAddition(Integer left, Integer right) {
        return left + right;
    }

    public Double doDoubleAddition(Double left, Double right) {
        return left + right;
    }

    public String doStringAddition(String left, String right) {
        return left + right;
    }
}


回答1:


For for your first question, I think you don't need to bind left and right values to T in ExpressionVisitor<T>.

You can set thenm as Object.

public class ExpressionVisitor<T> implements Visitor<T> {

    @Override
    public T visit(Expression expression) {
        return expression.accept(this);
    }

    @Override
    public T visit(BinaryExpression arithmeticExpression) {
        Operator op = arithmeticExpression.getOperator();
        Object left = visit(arithmeticExpression.getLeft());
        Object right = visit(arithmeticExpression.getRight());
        return op.apply(left, right);
    }

    @Override
    public T visit(ConstantExpression expression) {
        return (T) expression.getValue();
    }
}

Then you can have an Operator GreaterThan:

public class GreaterThan extends Operator{

    public Boolean doIntegerGreaterThan(Integer left, Integer right) {
        return left > right;
    }

    public Boolean doDoubleGreaterThan(Double left, Double right) {
        return left > right;
    }

    public Boolean doStringGreaterThan(String left, String right) {
        return left.length() > right.length();
    }
}



回答2:


As I understand it, you want to parse a textual expression to produce something that you can evaluate multiple times.

By the time you get that "something you can evaluate", the details of the expression language you parsed, including the tree structure, the kinds of operators, etc., should all be taken care of. Don't push those concerns into the code the just needs to evaluate.

So, your parsing process should produce an object that implements an interface something like this:

interface IExpression {
    double evaluateNumeric(Environment env);
    boolean evaluateLogical(Environment env);
    String evaluateString(Environment env);
    boolean isConstant();
}

Here, the Environment object holds the values of variables, function definitions, or whatever -- anything that expressions can use as input.

As you can see the interface allows any expression to be evaluated as a boolean (if used in an if, for example), as a number, or as a string. Depending on your expression language, some of these may throw UnsupportedOperationException. For something like JavaScript, for example, they would almost always all work.

You can also have methods that tell you things about the expression like isConstant. Depending on the language, it may be important to provide the type of object that the expression naturally produces.

To keep operation implementation concerns out of the parser, you may want to provide it with a factory interface like this:

IExpressionFactory {

    IExpression add(IExpression left, IExpression right);
    IExpression multiply(IExpression left, IExpression right);
    ///... etc. etc.
}

The parser would call a method in here for each sub-expression, to create it from its children.

The different kinds of subexpression can be implemented any way you like. In Java, I usually use classes for each category with lambda arguments like:

class ExpressionFactory : IExpressionFactory {
    ...
    IExpression add(final IExpression left, final IExpression right) {
        return arithmeticBinary(left, right, (a,b)->a+b);
    }
    ...
}

That saves you from having to having to write a class for each operator. When I'm super concerned about evaluation speed, I'll use inline classes with abstract bases.

IMPORTANT: Note that because a compiled IExpression doesn't expose the expression tree that was parsed to create it, you don't have to preserve that structure, and can do optimizations like constant folding:

IExpression arithmeticBinary(IExpression left, IExpression right, DoubleBiFunc operator) {
    if (left.isConstant() && right.isConstant()) {
        // The operands are constant, so we can evaluate this during compilation
        double value = operator.apply(
            left.evaluateNumeric(EMPTY_ENV),
            right.evaluateNumeric(EMPTY_ENV));
        return new NumericConstant(value);
    } else {
        return new ArithmeticBinaryExpression(left, right, operator);
    }
}

By processing as much as possible during compilation, you can make evaluation faster, by getting rid of all type/conversion checks, and performing various optimizations.



来源:https://stackoverflow.com/questions/61289076/expression-evaluation-using-visitor-pattern

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!