2
\$\begingroup\$

Type erasure is giving me nuts recently. I'm designing a class that performs symbolic differentiation on a math expression represented as a binary expression tree. The question is more on the design part than on the actual code part so I'm only giving out the method that looks awful to me.

public Node derive(final Node currentNode, Node parentNode) {
    Node dxNode = null;

    final Object cDataContext = currentNode.getData();
    if (Number.class.isAssignableFrom(cDataContext.getClass()))
        dxNode = new TreeNode<Double>(0.0);
    else if (AddOperator.class.isAssignableFrom(cDataContext.getClass()))
        dxNode = deriveAddContext((Node<AddOperator>) currentNode);
    else if (MulOperator.class.isAssignableFrom(cDataContext.getClass()))
        dxNode = deriveMulContext((Node<MulOperator>) currentNode);
    else if (SineFunction.class.isAssignableFrom(cDataContext.getClass()))
        dxNode = deriveSineContext((Node<SineFunction>) currentNode);

    if (dxNode != null && parentNode != null)
        dxNode.setParent(parentNode);

    return dxNode;
}

I think it already speaks for itself. I'm having methods with different names which is fine. The awful part at least for me is this huge if statement that I truncated for simplicity. Is there a better way of doing this? I mean I would love to live with dynamic dispatch having the whole derive method consisting of a simple:

Node dxNode = deriveNode(currentNode);
dxNode.setParent(parentNode);
return dxNode;

I guess Java won't give me this luxury so perhaps there is some design pattern that I can utilize here? Just to give you a better understanding of the algorithm I'll show a sample method:

private Node<AddOperator> deriveAddContext(final Node<AddOperator> additionContext) {
    // d/dx [f(x) + g(x)] = d/dx [f(x)] + d/dx [g(x)] => d/dx [f(x)] d/dx [g(x)] +

    // ROOT: ADD
    Node<AddOperator> dRoot = new TreeNode<AddOperator>(new AddOperator());
    // ROOT.LEFT: d/dx [f(x)]
    dRoot.setLeft(derive(additionContext.getLeft(), dRoot));
    // ROOT.RIGHT: d/dx [g(x)]
    dRoot.setRight(derive(additionContext.getRight(), dRoot));
    // RET: d/dx
    return dRoot;
}

So the whole algorithm is recursive on the expression traversing the original expr in an inorder fashion.

A Node has the following structure:

 dataField: <DataType>
 leftChild: Node
 rightChild: Node
 parent: Node
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Personally, I would be able to live with your huge if statement if it remains straightforward, is well tested, and hidden in a nice class. I think a parser will often have these types of structures, especially if you are generating it from a grammar using a tool (like ANTLR for example) instead of coding it by hand.

You could slightly improve the huge if statement by factoring out currentNode.getData().getClass() (instead of currentNode.getData()):

final Class<?> dataClass = currentNode.getData().getClass();
if (Number.class.isAssignableFrom(dataClass))
    dxNode = new TreeNode<Double>(0.0);
else if (AddOperator.class.isAssignableFrom(dataClass))
    dxNode = deriveAddContext((Node<AddOperator>) currentNode);
else if (MulOperator.class.isAssignableFrom(dataClass))
    dxNode = deriveMulContext((Node<MulOperator>) currentNode);
else if (SineFunction.class.isAssignableFrom(dataClass))
    dxNode = deriveSineContext((Node<SineFunction>) currentNode);

You could also consider using Java 8 to create an explicit mapping between the class the current node (data) is assignable from and the initialization code for dxNode using lambda expressions. This has the advantage that you should be able to extend the expression types by adding a single line to the map:

public Node deriveAlternative(final Node currentNode, final Node parentNode) {
    Node dxNode = null;

    final Map<Class<?>, Function<Node, Node>> deriveMap = new HashMap<>();
    deriveMap.put(Number.class, n -> new TreeNode<Double>(0.0));
    deriveMap.put(AddOperator.class, n -> deriveAddContext((Node<AddOperator>) n));
    deriveMap.put(MulOperator.class, n -> deriveMulContext((Node<MulOperator>) n));
    deriveMap.put(SineFunction.class, n -> deriveSineContext((Node<SineFunction>) n));

    final Optional<Class<?>> optionalKey = deriveMap.keySet().stream()
            .filter(key -> key.isAssignableFrom(currentNode.getData().getClass()))
            .findFirst();

    if (optionalKey.isPresent()) {
        final Class<?> key = optionalKey.get();
        dxNode = deriveMap.get(key).apply(currentNode);
    }

    if (dxNode != null && parentNode != null)
        dxNode.setParent(parentNode);

    return dxNode;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.