javagenericstype-inferencelogical-operatorsboolean-expression

Resolve Java Interface ambiguity in method parameters


I have a tree of boolean operations from a boolean expression that I have to run in Java. The tree would be something like this: The problem And I would need to run it in Java iteratively so that it returns me a boolean value.

boolean b = AND(isNumberEven("4"), AND(isNumberOdd("7"), isNumberEven("6"))

The problem is that I need to write the interfaces, since in my company we could create many classes like And, Or, isSomething, hasInArray (it could be any property needed by the other devs). We generally have two types of nodes: the logic ones, which have two branches, and the property checking ones, which are leafs of the tree.

What I tried to do below is something I think it's really wrong in Java, please help me doing it right.

My idea was to create two Interfaces, one for Logic operators and another for Property checking. The property checker needs to evaluate its property from the input, while the logic operator needs to evaluate the two branches and then compute something with the two results (like an AND or an OR, we don't care about the NOT operator for now). I was thinking of doing these interfaces

class PropertyCheckingInput {
   String s; // dummy property, this object is the thing we want to verify the property on (example, if this string is an even number)

   PropertyCheckingInput(...){...} // constructor for all params
}

interface PropertyChecking {
   boolean fire(PropertyCheckingInput i);
}

class LogicOperatorInput<
   T extends LogicOperator or PropertyChecking, // it can be one or the other
   Ti extends LogicOperatorInput or PropertyCheckingInput
> {
    T left;
    Ti leftInput;
    T right;
    Ti rightInput;

    // probabily the type of leftInput should act accordingly to the type of left, but I don't know how to enforce this in Java with generics

    LogicOperatorInput(...){...} // constructor for all params
}

interface LogicOperator{
   boolean fire(LogicOperatorInput i); 
}

In this case, if I want to implement something like the AND I could do it like

class And implements LogicOperator {
    boolean fire(LogicOperatorInput i) {
        i.left.fire(i.leftInput) && i.right.fire(i.rightInput);
    }

    And() {}

    public static void main(String[] args) {
        // expression: isNumberEven("4") AND isNumberOdd("7") AND isNumberEven("6")
        boolean b = new And().fire(new LogicOperatorInput(
            new isNumberEven(), 
            new PropertyCheckingInput("4"), 
            new And(), 
            new LogicOperatorInput(
                new isNumberOdd(), 
                new PropertyCheckingInput("7"), 
                new isNumberEven(), 
                new PropertyCheckingInput("6"))
        ));

        System.out.println(b);
    }
}

For each branch, I execute the function fire of both left and right with their input and care only about their result. Then if I want to create a boolean expression I concatenate the various nodes and inputs.

The compilers of course tells me this is wrong, because it cannot infer the correct type of the fire( ) functions.

I'm used to write in Javascript, which allows this type of stuff because it doesn't check what you're trying to do. Is there a way to do this in Java (I tried using some generics or abstract classes, but didn't work), or better yet a correct way to solve this problem (where we have a binary expression written as a binary tree and we want to resolve it by calling the class associated with each node)?


Solution

  • You might be overcomplicating this. If you want an expression tree like the one in the image, make an expression tree. You don't need to distinguish between "operators" and "input"s.

    // this represents a tree "node"
    interface BooleanExpression {
        // all nodes should implement this to represent how they are evaluated
        boolean evaluate();
    }
    
    // a node can be any of these types
    record IsNumberEven(int number) implements BooleanExpression {
        @Override
        public boolean evaluate() {
            return number() % 2 == 0;
        }
    }
    
    record IsNumberOdd(int number) implements BooleanExpression {
        @Override
        public boolean evaluate() {
            return number() % 2 == 1;
        }
    }
    
    record And(BooleanExpression left, BooleanExpression right) implements BooleanExpression {
        @Override
        public boolean evaluate() {
            return left().evaluate() && right().evaluate();
        }
    }
    

    Usage:

    boolean result = new And(
            new IsNumberEven(4),
            new And(
                    new IsNumberOdd(7),
                    new IsNumberEven(6)
            )
    ).evaluate();
    

    If you really want to separate out the "inputs" of the IsNumberOdd/IsNumberEven nodes, you can. Just generalise BooleanExpression to Expression<T>

    interface Expression<T> {
        T evaluate();
    }
    
    record Constant<T>(T value) implements Expression<T> {
        @Override
        public T evaluate() {
            return value();
        }
    }
    
    record IsNumberEven(Expression<Integer> number) implements Expression<Boolean> {
        @Override
        public Boolean evaluate() {
            return number().evaluate() % 2 == 0;
        }
    }
    
    record IsNumberOdd(Expression<Integer> number) implements Expression<Boolean> {
        @Override
        public Boolean evaluate() {
            return number().evaluate() % 2 == 1;
        }
    }
    
    record And(Expression<Boolean> left, Expression<Boolean> right) implements Expression<Boolean> {
        @Override
        public Boolean evaluate() {
            return left().evaluate() && right().evaluate();
        }
    }
    

    Usage:

    boolean result = new And(
            new IsNumberEven(new Constant<>(4)),
            new And(
                    new IsNumberOdd(new Constant<>(7)),
                    new IsNumberEven(new Constant<>(6))
            )
    ).evaluate();