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)
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.
In digital logic circuits simulation you will be forced to compute things transactionally - update all relevant outputs at once, - otherwise you will have interesting and subtle errors to debug.
For single clock domain designs it is best done using Haskell's data type like "data S a = S a (S a)", i.e., infinite lists where each element is a state of a "bus" at the beginning of the clock cycle. The register then can be defined as "register resetState inputs = S resetState inputs", addition as "addS (S x xs) (S y ys) = S (x+y) (addS xs ys)" and so on.
Then you can combine these "S-processors" in many interesting ways and throw Quickcheck at them with LTL specification of behavour (when input satisfies these conditions, then output must satisfy these) and, after investing some more time, even get working Verilog source for entire system.
My feeling is that better database-to-app-language integration is needed. Databases better handle big-picture issues, but integrating them with code is a pain in most frameworks because app world and database world ended up too different for unknown reasons.
It seems there was an anti-database fad(s) starting in the mid-90's that made app languages want to pretend databases didn't exist, to be magically hidden behind an interface. We are paying the price for this head-in-the-sand thinking. We should find a way to embrace databases rather than try to wrap them away, because that failed.
This assertion entirely misses the point.
In most applications which employ a db to handle persistence there is a need to map relational data to objects. Either you reinvent the wheel each time you need to handle data to/from the persistence layer, by rolling your own ORM infrastructure, or you reuse a component that is far less bug-prone and efficient and far easier to maintain and update. Framework developers recognized the need to free developers from wasting time writing bug-ridden and ineffixient boilerplate code, and thus started offering their own ORM frameworks. Real-world developers who felt the pain of having to roll their own ORM components or recognized the value of using those tools, whether in reducing bug count, increased performance, lower time to market, and lower maintenance needs.
Your comment sounds an awful lot like the comments from decades ago where random geeybeards whined that these new fangled compilers and interpreters want to pretend machine code doesn't exist, and how hand-written assembly is the right tool for the job.
I'm still looking for that. Where is it? I've yet to find a good ORM. They end up being dark grey arcane boxes that require dedicated specialists to troubleshoot and tame. It appears objects and RDBMS just don't mix well: https://en.wikipedia.org/wiki/Object-relational_impedance_mi...
I suggest we look at using more relational concepts on the application side instead of trying to translate back and forth between paradigms, spending too much grey matter and code on translating and converting back and forth.
Oracle Forms shows some possibilities. It requires about 1/5 the source code of OOP frameworks for similar CRUD applications. There are clunky aspects to Oracle Forms, I admit, but I believe the good stuff can be borrowed without also being forced to take the bad stuff. It's a starting point to app/DB integration exploration.
Pattern matching is the concept that they all have in common, that I miss when it isn't available. You don't have the 'exhaustiveness' check in Elixir because it's not possible in a dynamic language (dialyzer aside), but it's still a very powerful tool.
(match payment
[('invoice address ) (foo address )]
[('card card-details) (bar card-details)])
As you say, there's no exhaustiveness check; plus the language won't guarantee that the product types are adhered to. For example, someone might give us `('invoice card-details)` and the language wouldn't complain.Racket's contract system can help with this; although it's very slow.
Combine that with the proposed new enum syntax and existing sealed trait plus case class/object boilerplate will be gone. Looks pretty great, but we'll have to wait until next year sometime to use it in production.
Yes, I know types don't "have to" be hierarchical, but they are usually messy to manage as non-hierarchies.
And then the compiler will tell you all the stuff you have to do to implement a payment type. It might be a little or it might be a lot. Your ADT example also might be a little work or it might be a lot. I know some, or many, “frameworks” in the oop world might have that abstracted out n degrees for some rare or imaginary problem where you have to implement the abstractpaymetypefactoryfactory and all it’s constituents... to be fair that would ha e the ability to solve that rare or imaginary other problem though.
Traditional OO focused on extensibility over everything else. I think the idea of openness to extension is way oversold. The idea of a closed and simple to overview enumeration is much more common in my experience (after 25 years of OO...) and it’s sadly hell to create in C# compared to any language with ADTs.
A simple smell test for a programming language is this: if describing the above takes more than the above code - your language has a wart. 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. You'll be looking at likely 50-100 lines of code where no single line of code actually shows whan what you are modeling. This is the terrible part of OO. I'm sure there are a few cases to show an FP weakness handled elegantly by OO - but I'm equally sure those cases are fewer.
Also, there is nothing particularly "FP" about ADT's. They are just common in FP languages. Any OO language with a sum type and exhaustive matching can do this. But typically, such as for Java, C++ and C# they don't (yet). There are languages that support this that aren't FP (but not necessarily OO either) such as Rust and Kotlin.