When people talk about the "expression problem", what they're describing is the fact that, (in your example) if you add a new method to the trait, you have to go around and implement that new method on every type that implements the trait.
This is in contrast to if you had used an enum (sum type) instead, wherein adding a new operation is easy and can be done in a single place. But then in exchange, adding new variants requires going around and updating every existing pattern match to support the new variant.
Thanks. I wasn't thinking of enums. To the extent one designs a trait to use an enum type (or the enum to satisfy the trait), one wins. But it seems impossible to avoid code to handle all future {type, op} combinations. The nice thing I've seen with Rust is the ability to add to what's been done before without breaking what already works. I'm thinking of "orphan rules" here.
I'm thinking, didn't inheritance and abstract methods work to solve the problem?
I know inheritance has its own severe problems, but adding a generic abstract method at the base class could create reusable code that can be accessed by any new class that inherits from it.
P.S. ah ok, it's mentioned in the article at the Visitor section.
I think the problem is, that at the base class, you don't necessarily know how to handle things, that are encountered at the concrete class level, or don't want to put the logic for all implementations into your base class. That would defeat the purpose of your abstract class and the hierarchy.
If you have to handle specific behavior for the new method in an existing type, of course you will need to add a new implementation of the method for that type.
As I understand the Expression problem, the limitation of programming languages is that they force you to modify the previous existing types even if the new behavior is a generic method that works the same for all types, so it should be enough to define it once for all classes. A virtual method in the base class should be able to do that.
That only moves the problem to the base class. In this case there is no difference in effort modifying the base class to foresee all cases that could happen with the derived classes, compared to adapting all the derived classes. In fact, you might even be typing more, because you might need some more "if"s in that base class, because you don't know which specific implementation you are dealing with, for which you implement the behavior in the base class. You still have to deal with the same amount of cases. And it is still bad, when you need to extend something that a library does, that you are not maintaining.
This does not solve the issue at its core, I think.
This seems like it's easy to workaround by not modifying the existing trait but by defining a new trait with your new method, and impl'ing it atop the old trait.
That seems like a pretty ok 90% solution, and in a lot of ways cleaner and more well defined a way to grow your types anyhow.