> 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>).