Abstracting over Math Operations in an Interpreter in Java

I’m writing an AST Interpreter in Java have quite a few methods that check the argument types and perform a operation if they match. By now there are more than five methods that are basically copy pasted versions of each other. Is there a way to abstract the types to check and the operation to perform?

    @Override
    public Object visitMultiplyNode(MultiplyNode multiplyNode) {
        Object lhs = multiplyNode.getLeftHandSide().accept(this);
        Object rhs = multiplyNode.getRightHandSide().accept(this);

        if (lhs instanceof Double && rhs instanceof Double) {
            return (double) lhs * (double) rhs;
        }
        if (lhs instanceof Long && rhs instanceof Long) {
            return (long) lhs * (long) rhs;
        }
        throw new TypeError("Can not multiply " + lhs.getClass() + " and " + rhs.getClass() + ".");
    }

The types I want to check aren’t always to same, for example the modulus node only accepts Longs and the addition node also accepts Strings for concatenation.

    @Override
    public Object visitAddNode(AddNode addNode) {
        Object lhs = addNode.getLeftHandSide().accept(this);
        Object rhs = addNode.getRightHandSide().accept(this);

        if (lhs instanceof Double && rhs instanceof Double) {
            return (double) lhs + (double) rhs;
        }
        if (lhs instanceof Long && rhs instanceof  Long) {
            return (long) lhs + (long) rhs;
        }
        if (lhs instanceof String && rhs instanceof String) {
            return "" + lhs + lhs;
        }
        throw new TypeError("Can not add " + lhs.getClass() + " and " + rhs.getClass() + ".");
    }

    @Override
    public Object visitModulusNode(ModulusNode modulusNode) {
        Object lhs = modulusNode.getLeftHandSide().accept(this);
        Object rhs = modulusNode.getRightHandSide().accept(this);

        if (lhs instanceof Long && rhs instanceof Long) {
            return (long) lhs % (long) rhs;
        }
        throw new TypeError("Can not take modulus of " + lhs.getClass() + " and " + rhs.getClass() + ".");
    }

Answer

You could use a lambda:

private Object visitBinaryOperatorNode(BinaryOpNode node, BiFunction<T, T> op) {
    Object lhs = node.getLeftHandSide().accept(this);
    Object rhs = node.getRightHandSide().accept(this);

    if (lhs instanceof Long && rhs instanceof Long) {
        return op.apply((long) lhs, (long) rhs);
    }
    throw new TypeError("Can not take " + node.getOpName() + "of " + lhs.getClass() + " and " + rhs.getClass() + ".");
}

However, since you support multiple types for some of your operators, you need another layer of abstraction:

@RequiredArgsConstructor// Lombok, otherwise write the boilerplate yourself
public class BinaryOperator<T, T> {
   @NonNull private final BiFunction<T, T> op;
   @NonNull private final Class<T> clazz;

   public boolean isApplicable(Object left, Object right) {
       return clazz.isInstance(left) && clazz.isInstance(right);
   }

   public T apply(Object left, Object right) {
       return op.apply(clazz.cast(left), clazz.cast(right));
   }
}

You can now pass a bunch of valid binary operators and test if they’re applicable, and if so, apply them.

private static final List<BinaryOperator<?, ?>> VALID_ADD_OPERATORS = Arrays.asList(
    new BinaryOperator<>((x, y) -> x + y, Double.class), 
    new BinaryOperator<>((x, y) -> x + y, Long.class),
    new BinaryOperator<>((x, y) -> x + y, String.class)
);

private static final List<BinaryOperator<?, ?>> VALID_MULTIPLY_OPERATORS = Arrays.asList(
    new BinaryOperator<>((x, y) -> x * y, Double.class), 
    new BinaryOperator<>((x, y) -> x * y, Long.class)
);

@Override
public Object visitAddNode(AddNode addNode) {
    return visitBinaryOperatorNode(addNode, VALID_ADD_OPERATORS );
}

@Override
public Object visitMultiplyNode(MultiplyNode multiplyNode) { 
    return visitBinaryOperatorNode(multiplyNode, VALID_MULTIPLY_OPERATORS ); 
}

private Object visitBinaryOperatorNode(BinaryOpNode node, List<BinaryOperator<?, ?>> validOperators) {
    Object lhs = node.getLeftHandSide().accept(this);
    Object rhs = node.getRightHandSide().accept(this);

    for (BinaryOperator<?, ?> op : validOperators) {
        if (op.isApplicable(lhs, rhs)) return op.apply(lhs, rhs);
    }
    throw new TypeError("Can not take " + node.getOpName() + "of " + lhs.getClass() + " and " + rhs.getClass() + ".");
}