4 ms·
interface ExpressionOpen { T accept<T>(Evaluator<T> eval) => throw new NotImplementedException("Unsupported combination"); } interface ComplexExpressionOpen
by dwohnitmok 6y ago
interface ExpressionOpen { T accept<T>(Evaluator<T> eval) => throw new NotImplementedException("Unsupported combination"); }
interface ComplexExpressionOpen : ExpressionOpen { T accept<T>(ComplexEvaluator<T> eval) => throw new NotImplementedException("Unsupported combination"); }
Those don't really count though. Heck if you allow that I can write the code way shorter in true Haskell (not pseudo-Haskell) as well.
In particular
//we don't need any kind of boilerplate similar to evaluateExtendedExpression.
That was purely for combinatorial safety. You could remove it altogether if you wanted.
The whole point of the visitor pattern is to prevent you from being able to put unsupported combinations together. If you get rid of that requirement you don't need types at all and there are far far easier ways of doing this than either with the visitor pattern or with discriminated unions (just do it all with nested dictionaries for a fraction of the code).
Throwing this exception is basically a cheat is it not?
> support for recursive types
I think every statically typed language I know has support for recursive types. Certainly C# does.
> pattern matching
Eh... I'm not really using pattern matching in any real sense here. They could be replaced by if statements. The only reason it's there is because there's total type erasure in Haskell so runtime dynamic dispatch isn't really a thing in the same way it is in C#.
- tsimionescu 6y ago> Those don't really count though. Heck if you allow that I can write the code way shorter in true Haskell (not pseudo-Haskell) as well. We could get rid of those base cases; we would then have to make ComplexExpression not a subtype of ExpressionOpen, and that in turn would mean we can't extend any of the extended evaluators from the old evaluators. This would just add the same boilerplate you have (copying the StringEvaluator implementation in StringEvaluatorExtended etc), all to prevent multiply.accept(printEvaluator). I'm not sure this extra bit of type safety would be worth it, but it certainly depends on the specifics. > The whole point of the visitor pattern is to prevent you from being able to put unsupported combinations together. While that is an important part, this is not an all-or-nothing problem. Type safety is a spectrum, and we can choose where to give up some safety to gain something else.
- dwohnitmok 6y ago> Type safety is a spectrum, and we can choose where to give up some safety to gain something else. I agree, but I'm saying that if you're willing to give up the type safety of verified combinations, you can have a solution that is just as (or even more) extensible as the visitor pattern (or discriminated unions) that is fewer lines of code and might even perform faster. Namely you can just downcast in the evaluator to the cases you can handle and then just bail if the downcast fails. This is the exact same functionality, can be extended in the same way (and in fact in more ways), and is fewer lines of code. As far as I can tell the only reason you'd want to use the visitor pattern (or discriminated unions) here is some notion of "purity," e.g. in the case of OO that you're violating some notion of the Liskov Substitution principle by explicitly downcasting and in the case of FP that you're violating parametric polymorphism. But neither notions of purity feel particularly convincing to me. As an aside > This would just add the same boilerplate you have In fact I think in the C# version it would be even more code because you can't reuse the same match patterns; you'd have to repeat way more of each evaluator.
- tsimionescu 6y ago> As far as I can tell the only reason you'd want to use the visitor pattern (or discriminated unions) here is some notion of "purity," e.g. in the case of OO that you're violating some notion of the Liskov Substitution principle by explicitly downcasting and in the case of FP that you're violating parametric polymorphism. But neither notions of purity feel particularly convincing to me. In an OO language, the reason you'd prefer the Visitor pattern to downcasting is that it allows extending behavior by simply declaring new subtypes, instead of having to edit the central evaluator. This is why I keep insisting that the Visitor pattern is about double virtual dispatch, not about discriminated unions. > In fact I think in the C# version it would be even more code because you can't reuse the same match patterns; you'd have to repeat way more of each evaluator. Here is the C# version without the default NotImplementedException escape hatch: // ------ BEGIN : In some external library interface ExpressionOpen { T accept<T>(Evaluator<T> eval); } record Literal(int n) : ExpressionOpen{ public T accept<T>(Evaluator<T> eval) => eval.visitLiteral(this); } record Addition(ExpressionOpen x, ExpressionOpen y) : ExpressionOpen { public T accept<T>(Evaluator<T> eval) => eval.visitAddition(this); } interface Evaluator<T> { T visitLiteral(Literal l); T visitAddition(Addition a); } class StringEvaluator : Evaluator<string> { public virtual string visitLiteral(Literal l) => l.n.ToString(); public string visitAddition(Addition a) => a.x.accept(this) + "+" + a.y.accept(this); } class ExecutionEvaluator : Evaluator<int> { public int visitLiteral(Literal l) => l.n; public int visitAddition(Addition a) => a.x.accept(this) + a.y.accept(this); } // ------ END : No longer in some external library // ------ BEGIN : My own code //note: no need for ComplexEvaluator here class HexadecimalEvaluator : StringEvaluator { public override string visitLiteral(Literal x) => x.n.ToString("x"); // thanks to inheritance, the existing expressions can already accept this // so no need for an equivalent for extendedEvaluator; // we're also inheriting visit(Addition) but that is not fundamental } interface ComplexExpressionOpen { T accept<T>(ComplexEvaluator<T> eval); } record Multiplication(ExpressionOpen x, ExpressionOpen y) : ComplexExpressionOpen { public T accept<T> (ComplexEvaluator<T> eval) => eval.visitMultiplication(this); } //but we do need ComplexEvaluator to go with ComplexExpression interface ComplexEvaluator<T> : Evaluator<T> { T visitMultiplication(Multiplication x); } class StringEvaluatorExtended : ComplexEvaluator<string> { public virtual string visitLiteral(Literal l) => l.n.ToString(); public string visitAddition(Addition a) => a.x.accept(this) + "+" + a.y.accept(this); public string visitMultiplication(Multiplication m) => m.x.accept(this) + " * " + m.y.accept(this); } class ExecutionEvaluatorExtended : ComplexEvaluator<int> { public int visitLiteral(Literal l) => l.n; public int visitAddition(Addition a) => a.x.accept(this) + a.y.accept(this); public int visitMultiplication(Multiplication m) => m.x.accept(this) * m.y.accept(this); } class HexadecimalEvaluatorExtended: StringEvaluatorExtended { public string visit(Multiplication m) => m.x.accept(this) + " * " + m.y.accept(this); public override string visitLiteral(Literal x) => x.n.ToString("x"); } //we don't need any kind of boilerplate similar to evaluateExtendedExpression. // ------ END : Finished with my own code However, this version has a major problem that neither your version nor my initial one do: I can't represent something like Addition(Multiplication(1,2),3). I don't think this problem is fixable in .NET, since you can't construct generic types that depend on runtime values, such as an ExpressionOpen<V> where V is anything which implements Evaluator<T>. We could probably hack around it with a lot more boilerplate and some convention (an associated Evaluator interface that Evaluator<T> extends, and explicit casts from Evaluator to Evaluator<T> in the accept methods, plus a convention that no one should implement Evaluator except Evaluator<T>).