Saturday, June 16, 2012

Movimentum - Being a Nice Host: Visitors

The design for the constraint and expression visitor is straightforward. However, with all visitors, I opt for a generic input parameter and a generic return type to get a more versatile facility:

    public interface ISolverModelConstraintVisitor<in TParameter, out TResult> {
        TResult Visit(EqualsZeroConstraint equalsZero, TParameter p);
        TResult Visit(MoreThanZeroConstraint moreThanZero, TParameter p);
        TResult Visit(AtLeastZeroConstraint atLeastZero, TParameter p);
    }

    public interface ISolverModelExprVisitor<in TParameter, out TResult> {
        TResult Visit(Constant constant, TParameter p);
        TResult Visit(NamedVariable namedVariable, TParameter p);
        TResult Visit(AnchorVariable anchorVariable, TParameter p);
        TResult Visit(UnaryExpression unaryExpression, TParameter p);
        TResult Visit(BinaryExpression binaryExpression, TParameter p);
        //TResult Visit(RangeExpr rangeExpr, TParameter p);
    }

I also want to visit the operators, so we could add a simple Operator visitor:

    public interface ISolverModelOpVisitor<in TParameter, out TResult> {
        TResult Visit(Plus op, TParameter p);
        // ...
        TResult Visit(UnaryMinus op, TParameter p);
        TResult Visit(Square op, TParameter p);
        // ...
    }

However, we will need to pass in visited results to the operators (for example, expression evaluation will first require an evaluation of sub-expressions; and then passing the results to the operator). Therefore, we give the operator visitors aditional parameters for expressions (yes, this is a little bit of "up-front-design"—but hey, I know that I will need it!):

    public interface ISolverModelUnaryOpVisitor
            <in TExpression, in TParameter, out TResult> {
        TResult Visit(UnaryMinus op, TExpression inner, TParameter p);
        TResult Visit(Square op, TExpression inner, TParameter p);
        TResult Visit(FormalSquareroot op, TExpression inner, TParameter p);
        TResult Visit(PositiveSquareroot op, TExpression inner, TParameter p);
        //TResult Visit(Integral op, TExpression e, TParameter p);
        //TResult Visit(Differential op, TExpression e, TParameter p);
        TResult Visit(Sin op, TExpression inner, TParameter p);
        TResult Visit(Cos op, TExpression inner, TParameter p);
    }

    public interface ISolverModelBinaryOpVisitor
            <in TExpression, in TParameter, out TResult> {
        TResult Visit(Plus op, TExpression lhs, TExpression rhs, TParameter p);
        TResult Visit(Times op, TExpression lhs, TExpression rhs, TParameter p);
        TResult Visit(Divide op, TExpression lhs, TExpression rhs, TParameter p);
    }

Of course, we need also the counterpart Accept methods in the model classes. They are straightforward—here are a few of them, the rest looks exactly alike:

    #region Input constraints

    public abstract partial class AbstractConstraint {
        public abstract TResult Accept<TParameter, TResult>(
            ISolverModelConstraintVisitor<TParameter, TResult> visitor, TParameter p);
    }

    public partial class EqualsZeroConstraint : ScalarConstraint {
        public override TResult Accept<TParameter, TResult>(
            ISolverModelConstraintVisitor<TParameter, TResult> visitor, TParameter p) {
   
            return visitor.Visit(this, p);
        }
    }

    // same for the other two constraints types.

    #endregion Input constraints

    #region Expressions

    public abstract partial class AbstractExpr {
        public abstract TResult Accept<TParameter, TResult>(
            ISolverModelExprVisitor<TParameter, TResult> visitor, TParameter p);
    }

    public partial class Constant : AbstractExpr {
        public override TResult Accept<TParameter, TResult>(
            ISolverModelExprVisitor<TParameter, TResult> visitor, TParameter p) {
   
            return visitor.Visit(this, p);
        }
    }

    public partial class NamedVariable : Variable {
        public override TResult Accept<TParameter, TResult>(
            ISolverModelExprVisitor<TParameter, TResult> visitor, TParameter p) {
   
            return visitor.Visit(this, p);
        }
    }

    // same for AnchorVariable

    public partial class UnaryExpression : AbstractExpr {
        public override TResult Accept<TParameter, TResult>(
            ISolverModelExprVisitor<TParameter, TResult> visitor, TParameter p) {
   
            return visitor.Visit(this, p);
        }
    }

    public abstract partial class UnaryOperator : AbstractOperator {
        public abstract TResult Accept<TExpression, TParameter, TResult>(
            ISolverModelUnaryOpVisitor<TExpression, TParameter, TResult> visitor,
            TExpression innerResult,
            TParameter p);
    }

    public partial class UnaryMinus : UnaryOperator {
        public override TResult Accept<TExpression, TParameter, TResult>(
            ISolverModelUnaryOpVisitor<TExpression, TParameter, TResult> visitor,
            TExpression innerResult,
            TParameter p) {
   
            return visitor.Visit(this, innerResult, p);
        }
    }

    // same for all other UnaryOperators

    public partial class BinaryExpression : AbstractExpr {
        public override TResult Accept<TParameter, TResult>(
            ISolverModelExprVisitor<TParameter, TResult> visitor, TParameter p) {
   
            return visitor.Visit(this, p);
        }
    }

    public abstract partial class BinaryOperator : AbstractOperator {
        public abstract TResult Accept<TExpression, TParameter, TResult>(
            ISolverModelBinaryOpVisitor<TExpression, TParameter, TResult> visitor,
            TExpression lhsResult, TExpression rhsResult, TParameter p);
    }

    public partial class Plus : BinaryOperator {
        public override TResult Accept<TExpression, TParameter, TResult>(
            ISolverModelBinaryOpVisitor<TExpression, TParameter, TResult> visitor,
            TExpression lhsResult, TExpression rhsResult, TParameter p) {
   
            return visitor.Visit(this, lhsResult, rhsResult, p);
        }
    }

    // same for all other BinaryOperators

    #endregion Expressions

(I am not really happy about the formatting of the parameters—but this blog is somewhat too narrow to write beautiful code, or so I claim).

One last thought: The whole idea of visitors is the consequence of a separation of concerns: Different concerns, or aspects, should be in different classes. However, with the advent of partial classes in C#, one could "go back" to putting more into a single class, and merely distribute the "concerns" into different source files. The standard answer to this is that a visitor need not see internal details of the visited class, therefore, the separation into different classes leads to better information hiding. However, in all examples I have seen, virtually all structural information of the visited classes has to be published for visitors. Many years ago, this lead a colleague of mine to the observation that the name "visitor" is not derived from an amicable visit, but from the meaning of "visit" that implies an examination of the visited item—e.g. when a general pays a "visit" to some military base.
In German, the sarcasm is even more pronounced, because there is the word "Leibesvisitation", meaning "thorough body investigation" by a police or customs officer.
One consequence of visitors is the normalization of the Visit interface. For my visitor definitions, that means "exactly one parameter in—exactly one result out" (in the next posting you will see that this requires a funny "Ignore" class for simple visitors). One can view this normalization as a (small) benefit, because one design decision for a group of collaborating model methods is predefined. But of course, when one needs more parameters an especially out or ref parameters, the fixed Visit methods become a curse.

One advantage of the visitor pattern is that, with a modern IDE, it is very easy to create a new visitor class. Adding similar methods directly to the model classes is harder, unless one keeps around a template file for partial model classes with visiting methods and expands it using an editor or some sort of script.

Anyway, I'll keep to the visitor pattern in this project.

No comments:

Post a Comment