The good news is that many languages seem to be cozying up to them, and both the JVM (through Kotlin, Scala, et all) and .net (through F# or C# w/ language-ext) support them.
Even better news is that the C# team has stated that they want to implement Sum Types and ADTs into the language, and are actively looking into it.
I dislike dynamic languages where parameters and variables can take on any type -- it's rarely the case that same variable/parameter would ever need to contain a string, a number, or a Widget in the same block of code.
I find it much more freeing to have the compiler be in charge of exactness so I can make whatever changes I need knowing that entire classes of mistakes are now impossible.
And sum types aren’t for “radically different types”. You can define an error type to be one of different options (I.e. a more principled error code), or to represent nullability in the type system, or to indicate fallibility without relying on exceptions, etc.
Rust uses all of these to great effect, and does so because these sum types are generic.
It's a good software engineering principle to make control and data flow as streamlined as possible, for similar data. Minimize branches and special cases. Generics help with this, they hide the "irrelevant" differences, surfacing only the relevant.
On the other hand, if there are _actually_ different cases, that need to be handled differently, you want to branch and you want to express that there are multiple choices. Sum types make this a compiler-checked type system feature.
let v = match map.entry(key) {
Entry::Occupied(o) => {
o.get_mut() += 1;
o.into_mut()
}
Entry::Vacant(v) => {
update_vacant_count(v.key());
v.insert(0)
}
};
I view sum types as enabling the exact same exactness as you describe in your last line; especially since you can easily switch/match based on a specific subtype if you realize you need that, without adding another method to the base class and copying into the x subclasses that you have for implementing the different behavior.[0]: https://doc.rust-lang.org/std/collections/hash_map/enum.Entr...
Let's say you're opening a file that you think is a CSV. There can be several outcomes:
- the file doesn't exist
- the file can't be read
- the file can be read but isn't a valid CSV
- the file can be read and is valid, and you get some data
All of these are different types of results. You can get away with treating the first 3 as the same, but not the last. Without a tagged union, you'll probably resort to one of a few tricks:
- You'll have some sort of type with an error code, and a nullable data field. In reality, this is a tagged union, it's just that your compiler doesn't know about it and can't catch your errors.
- you'll return an error value and have some sort of "out" value with the data: this is basically the same as the previous example.
- you'll throw exceptions, which usually ends up with people writing code that forgets about the exception because the compiler doesn't care about it, and the code works 99% of the time until it completely blows up.
So, I mean, forget 'the next decade or two'; the majority of people are doing this right now; python and js are the probably the two most popular languages in use right now.
Will it end up in all statically typed languages? Dunno; I guess probably not in java or C# any time soon, but swift and kotlin support them already).
...ie. if your excuse for not wanting to learn it is that it's probably an edge case that most people don't have to care about now, and probably never will, you're mistaken I'm afraid.
It's a style of code that is very much currently in use.
Yes.
For example, some use cases: https://www.typescriptlang.org/v2/docs/handbook/unions-and-i...
This sort of code is very common.
I really don't know what more to say about this; if you don't want to use them, don't. ...but if your excuse for not using them is that other people don't, it's wrong.
They also tend to defer handling out into places where the context isn't always there.
The trend in language design does seem to be more broadly away from exceptions for this kind of thing and into generic pattern matching and status result types.
> Partially because people just get in the lazy habit of catch and rethrow RuntimeException, or catch and log, etc. when confronted with them.
After quite a while of thinking this way, I came to the conclusion that:
95% of the time, there's no way to 'handle' an error in a 'make it right' sense. Disk write failed? REST request failed? DNS lookup? There usually isn't an alternative to logging/rethrowing.
When there is a way to handle an error (usually by retrying?), it's top level anyway.
Furthermore, IO is the stuff that can just 'go wrong' regardless of how good the programmer is, and IO tends to sit at the bottom in most Java programs. This means every method call is prone to IOExceptions.
https://verraes.net/2014/12/erlang-let-it-crash/
If IOException on a read is truly happening, and it isn't just a case of a missing file, there are serious issues that aren't going to be fixed with a catch-and-log, or be able to be handled further up the call stack.
They're not popular in Java though, because the ergonomics is a lot worse than working with a Result type.
It doesn't hurt that static languages (TypeScript) or tools (mypy) that lightly lay on top of dynamic languages often do support sum types.