It's the main thing I've missed from OCaml when programming in Haskell (you can sort of do something like this with typeclasses, but I much prefer the OCaml solution).
- All instances are thrown into one global namespace, so we don't need to worry about where to find the right one.
- There can be at most one instance per type. Hence there's no ambiguity about which one to pick.
These make it easy for the machine to pick the correct instance automatically. Unfortunately, it also means that every time we import or upgrade a library, there's the chance that it will break things by exposing an instance that clashes with what we have.
The "backpack" stuff looks interesting in this regard, although it seems very over-engineered (mostly due to legacy concerns).
"Extend program with new data types and new functions"
It is quite well studied and solved in various languages. And, in my opinion, typical OO language solution does not come as especially succinct.
I wouldn't say the expression problem is "solved"; various languages pick different tradeoffs, but the underlying problem is still there.
The best description I've seen of this is at https://www.tedinski.com/2018/02/27/the-expression-problem.h...
The same is true in java if you use instanceof. The thing is, just like enums inductive data type definitions are not supposed to be extended by any module other than the one declaring them, and adding more should be considered an API change.
Java isn't a particularly pure example of OOP: it has `if`/`for`/`while`/etc. from structured programming, non-OO "primitive types", non-polymorphic `+`/`-`/`*`/etc. and so on. `instanceof` is another example of a non-OOP feature (the OOP approach would be an `isFoo` method, where each class can decide what to return).
> inductive data type definitions are not supposed to be extended by any module other than the one declaring them, and adding more should be considered an API change.
Exactly. Yet in OOP, anyone can add new subclasses whenever they like, without ever telling the original author, and instances of those subclasses can co-exist alongside any other subclasses and will (in theory) work fine with all the existing code written for the superclasses.
What if I altered your sentence to say the following:
"Functions acting on a datatype are not supposed to be extended by any module other than the one declaring that type, and adding more should be considered an API change."
This would seem crazy if we applied it to an FP language; yet that's exactly the case in OOP, where a fixed set of methods are implemented inside each class.
This is the "expression problem" https://en.wikipedia.org/wiki/Expression_problem
In FP with ADTs, anyone can write new functions for a datatype. Yet, as you say, adding a new case requires modifying the definition and existing functions.
In OOP, anyone can add a new case, by defining a new subclass. Yet writing a new method for that class requires modifying the definition and existing subclasses.
One of these isn't always better than the other, so it's useful to have both.
> If you try to describe this properly in C# you'll be writing an abstract outer class and N concrete inner classes, plus boilerplate for comparisons and so on.
This overhead is due to emulating one approach using another. It would also take a bunch of boilerplate to implement subclassing with ADTs (e.g. a sum type to describe the methods, a smart constructor to implement the inheritance, etc.).
(Personally I prefer ADTs too, but they're not a magic bullet)