To put it plainly, Java is a fundamentally broken language and its mistakes, starting from not having sum/option/result types from day 1, make it impossible to fix.
edit to respond: calling them "bells and whistles" is a deep misunderstanding of what sum types are. They profoundly change how every single piece of code in the language is written, starting from how nulls are handled, and cannot be retrofitted into an existing ecosystem. (You can add them later, but baking in sum types from the start is very different. In particular Java would not have its terrible exception system if it had sum types.)
To be fair, Java has always supported a particular flavor of sum types founded on subtyping: use an abstract class with a private constructor as a base, add nested static final subclasses for the variants, and decide whether to use instanceof or the Visitor pattern to dispatch on each variant. (The Visitor approach corresponds to a certain isomorphism -- the sum type `A + B` is equivalent to `forall T. (A -> T, B -> T) -> T`, where the pair of handlers `(A -> T, B -> T)` is the titular visitor.)
That JEP elaborates on the same ideas (notably using `instanceof` instead), as the linked JEP explains, but the motivation for sealed classes extends to further harmony with subtyping. The major item seems, to me, to be allowing interfaces to be a supertype. A minor but distinctive item is that the sum type's variants can be defined at (some) distance from the base type, rather than all being defined in a single breath.
The problem is, even though you could always express sum types in Java, it's verbose, unpleasant, and above all "clever" in a way unnecessary in other languages. I think this JEP helps somewhat -- the private constructor trick in particular goes away -- but it doesn't really help with clients of your sum type. Pattern matching is still yet to come, and if you want a Visitor, you still need to write a Visitor.
To the grandparent's point, this JEP isn't enough to provide a coherent alternative to checked exceptions or other kinds of structured control flow.
Every language is fundamentally broken from day 1, whether it is a known tradeoff or something that remains hidden for several years.
Also, even if you had the perfect language with a presumably not-mainstream feature, you would have to make that feature somewhat known in the developer ecosystem (just look at Rust’s borrow checker, it does cause issues for newcomers as opposed to features already having an analog in other languages). While FP ideas were known before Java (ML predates it by quite a few years), it has become only recently more acceptable, so it makes sense to only know incorporate it into the language (and yet again, do note that FP is not a silver bullet so java has to let developers write imperative constructs just as well as before)
Unlike first-class monads, sum types are not some sort of esoteric FP construct that arises from challenging technical problems around dealing with external state. They are the very obvious dual of product types, which every language has some version of. They are easy to understand, require very little tutorial-ing, and their power is immediately obvious when you start using them.
Product types model "x and y", sum types model "x or y". One of them is universal. The other has been totally missing from mainstream languages until recently. No wonder our software sucks so much.
I agree with you in that I really like and really miss them from languages since most of them doesn’t have it in a feasible way. But sealed classes are coming fortunately in Java at least.
Yes, Go is a fundamentally broken language too. As is C++, as is C. Sum types are the absolute bare minimum a language must have for me to not consider it broken beyond hope.
Go in particular is really unfortunate for a language so new. The language's weaknesses have dropped my estimation of its creators by several notches.
Yes, that's possible. I would rather tear my eyes out than use a language without sum types. The benefits of sum types in being able to model arbitrarily complex domains are extraordinarily massive.
Every language has product types. Every programmer understands how useful product types are. Why in the world would the same not apply to sum types?
You sound like you have a hammer, and so every problem looks like a nail.
Sum types can be useful. Other aspects of languages can be useful, too. Why fixate on that one? And if you're going to say that it's a minimum bar, well, some of the other features are missing in most languages that have sum types.
Pick the language that has the total set of features that makes it easiest to write whatever program you're trying to write. Don't get locked in to focusing on only one feature.
I work with Java in my day job; it's by no means my favorite language, but I can be quite productive in it.
I haven't kept a tally of my frustrations with Java, but I can assure you that the most frequent is its lack of first-class sum types. Every time I need to encode (encode!) such a basic concept as "it could be this or that" using subclasses and some Design Pattern to Manage the Variants (usually Visitor, sometimes State), I metaphorically weep for my soul.
There are plenty of other minor gripes I have with the language -- there have been multiple instances where generic generics (HKTs, `class Foo<F<_>>`) would have made things much clearer -- but sum types are certainly the most prominent.
Late reply; I hope you see it. My reply is late because I had to think for a day first. So, my sincere congratulations - a post that makes me think for a day is much rarer than a post I agree with.
To me, sum types for error handling are isomorphic to checked exceptions. They both let you do dual-track programming - separating the normal path from the error path. Both have compiler support for enforcement. But checked exceptions are no longer considered to be the answer. What went wrong?
The problem turned out to be the programmers. They did at least two things that subverted checked exceptions.
First, they silently ate exceptions (that is, had an empty catch block just to make the exception go away. This is the equivalent of having a sum type that is either an integer or Nothing, and a function. Rather than return the sum type, the function returns an integer. In the Nothing case, the function just returns 0. That's about the same as the empty catch block to not have to declare the exception in the function's return type.
The second way checked exceptions went wrong was the opposite. When a function could throw Exception1, Exception2, Exception3, and Exception4, it was tempting to just declare it as throwing Exception (the base class). In the same way, a function that gets SumType1 back from one function, SumType2 back from another function, and SumType3 from a third function may return the sum of the sum types. It becomes an Everything type.
In both cases, the problem was that programmers were lazy. But here we are 20 years later, and programmers are still lazy. Until the programmers change, sum types won't fix things any more than checked exceptions did.
Thanks for the thoughtful response. I spent a bit of time thinking about it, and thank you for that as well.
So, I think you're right about a result type being isomorphic to checked exceptions, for the reasons you laid out. If you compare it to Rust, what ends up happening is similar -- many libraries return an error type that's a union of all their dependencies' error types, and binaries end up using an "everything" anyhow::Error type in the end. However, I think where sum types in general end up working and checked exceptions don't are:
1. Sum types don't encourage a mix of checked and unchecked exceptions the way Java does. Translating from Rust, all exceptions that are meant to be handled by regular users are checked. The only exceptions that are unchecked are broken invariants (panics), which usually end up being handled either through aborting the program or through some sort of top-level restart logic. You could write Java in that style but it's not the ecosystem's convention.
2. Sum types are more general than checked exceptions: they can be used to express nulls and business logic as well. I suspect checked exceptions would have worked better if Java also had nullable and non-nullable types, because nullability is such a common source of errors.
3. You're right that you can drop an error on the floor with sum types as well, just like an empty catch block with exceptions. But that just doesn't happen nearly as often in practice, because result types form a closed set. With checked exceptions, in practice you often end up with a method throwing both checked and unchecked exceptions, and the same syntax is used to handle both. I think checked and unchecked exceptions are fundamentally very, very different and mixing the two is a mistake.
I wouldn't say no, because it's built with the right spirit in mind. But at the same time, it's different from languages have native support for sum types. Advantage of sum type is that it can be matched exhaustively like an enum, but at the same time it's flexible to contain any data like a data class.
Having a type available in an optional library is not the same thing as having them baked in from the beginning. The absence of sum types warps a language in grotesquely terrible ways, such as Go's err != nil patterns or Java's exception system and visitor patterns.
Improving a dominant (but broken) language is called "innovation" and "improvement". There's no reason all your magic bells and whistles couldn't have been added into Java. Creating new languages just made life worse for everyone.
>There's no reason all your magic bells and whistles couldn't have been added into Java.
Because it's designed in a way that doesn't always allow these bells and whistles.
If I were to tell you that we should have kept using horse-drawn carriages, and that any of the fancy bells and whistles that cars have could be added to horse-drawn carriages, you would probably look at me with confusion.
> Improving a dominant (but broken) language is called "innovation" and "improvement"
Sure? But modifying an existing (and for most of its history proprietary) language can be a difficult, potentially-political move. If I have an idea on how to do, I don't know, linear-types checked at compile-time, I wouldn't have any idea where to begin adding that to the javac compiler. It might be easier to build my own little language to build it, and then hope that maybe the Oracle devs decide to pick it up.
The horse-drawn carriages analogy is not applicable because I'm using Java in my current project, like most other large corporations, I don't find it antiquated, I find it "state of the art".
There are some improvements that can be made however (any language can be improved on), that would require minimal breaking changes (like fixing Type-Erasure), and I'd even be fine with minimal breaking changes that break backwards compatibility every 5 years or so.
The problem is when people say "Let's burn it all down and start over, so zero existing code is salvageable."
Sure, great, but you must understand that adding language features to basically anything that isn’t Lisp moves glacially at best.
I mean, let’s go with your example with type erasure; people have been complaining about it for more than a decade, and Oracle still hasn’t fixed it. The Java language designers aren’t stupid, and I’m sure they’ve read the complaints, but it still is almost universally agreed upon to be a broken feature of the language.
It’s almost never clear if “language feature X” is a good idea until it’s been implemented and battle-tested. If I have a new idea on how to build something in a compiler and I’m not sure if it’s a good idea, are you saying that the best path forward would be for me to make a PR to javac instead of building a proof of concept language?
Replying to my own post because it's too late to edit and I'd like to clarify a bit.
I actually do agree that as software engineers, we're often a bit too eager to reinvent wheels. While I am not a huge fan of Java, if I owned a company, I would probably be more likely to use a JVM language (probably Clojure) than I would to use something like Haskell or Idris, precisely for the reasons you've discussed.
My overall point, though, is that often times languages themselves lag behind the state-of-the-art; there's been a lot of progress made in language design, and Java (and a lot of other languages) can feel crufty in the process. Sometimes the wheel really does need to be reinvented...if we could somehow convince the entire industry to use Lisp, this would (arguably) be a non-issue, since they allow you to abuse macros relatively easily and add language features (see CLOS or core.async for examples), but Lisp hasn't really taken over the world like I wish it would.
Thanks for your two posts, I agree with pretty much all of what you're saying.
When you look at the millions of man-hours that have gone into the "burn it all down and start from scratch" languages (Go, Rust, etc) and you consider what if those guys had written new compilers for JAVA syntax? How great that would be. They could still accomplish many Go and Rust objectives without having to completely invent their own non-Java-like syntax. Even something that was "Java-like" is better than burning it all down every time something new is needed.
I don't think I agree with your last point; I actually think that the Java syntax isn't particularly great, even for the time. I don't think that Java's idea of object oriented design is ideal (I'm more of a fan of the ObjectiveC/SmallTalk model), and that version of OOP is all Java really contributes from a syntax perspective...otherwise it's largely just C/C++'s syntax.
However, in a sister thread I think you mentioned that you are basically alright with languages targeting the same VM, which I think is probably a better path forward for a majority of use-cases. Clojure and Kotlin and Scala all benefit from being more-or-less fully interoperable with each other; as a result, one can feel free to experiment with language design to their heart's content without too much fragmentation.
That said, I don't know that it's entirely fair to completely criticize Rust on this; Rust exists specifically to address issues with C and C++, languages without garbage collection, and whose design doesn't quite allow the same level of compiler safety and goodness that Rust does, though to be fair Rust does have C FFI so it's not necessarily always reinventing the wheel either. I mean, I agree with the blog post we're chatting on top of; Rust might be super awesome for systems-ey stuff, but for anything TCP-or-higher, I think a managed language is kind of better.
edit to respond: calling them "bells and whistles" is a deep misunderstanding of what sum types are. They profoundly change how every single piece of code in the language is written, starting from how nulls are handled, and cannot be retrofitted into an existing ecosystem. (You can add them later, but baking in sum types from the start is very different. In particular Java would not have its terrible exception system if it had sum types.)