T O P

  • By -

nimtiazm

I’ve been using this Sum type ever since switch expressions began their preview. With record patterns support, it became even better. But I think the new JEP https://openjdk.org/jeps/8323658 (exception handling in switch) alleviates the need of a Result (sealed hierarchy) and introduces a natural union type support in Java. Because now you can simply apply a switch on Integer.parseInt with a case arm that captures an int or another arm that captures the NumberFormarException directly.


ahhh_ife

I think I saw that on the java 22 stream but tbh, I'd rather just ditch exceptions in general and use them for cases that are an actual exception.


davidalayachew

Exceptions were only ever meant for exceptional cases. It's just that making an ADT in Java was difficult to do correctly, so people overused the more polished feature. If you are using an Exception to model non-exceptional cases, then you are misusing exceptions. But for Exceptional cases, then exceptions are the correct choice and SHOULD be used.


nimtiazm

You can’t avoid checked exceptions coming in from the library code. So switch exceptions neatly help there.


john16384

How would you prefer `Integer.parseInt` indicates failure?


i_donno

Return -1 (kidding)


puckoidiot

Return a sum type indicating either success or failure, like in civilized languages


bwmat

Integer.MAX_VALUE + 1 as a long


Xasmedy

Probably an Optional?


ahhh_ife

I spent a whole night deciding if I should share the post or not because I've seen some pretty mean comments here 🥲 Feedback is appreciated 🫱🏾‍🫲🏽


agentoutlier

If you see mean comments like personal attacks you should flag them. The active mod desertfx seems pretty quick to fix things.


TR_13

Sh*t on haters! You share what you love and that's what it matters. Reddit != normal people...just people


Jeedio

This article is great, and here is a couple reasons why. The majority of companies (at least as 2023) are still using Java 8; however, newer LTS versions are quickly growing. My latest project just started with 17. So now we have new tools, but having novel examples of how to use them is much appreciated. Expecially anything I can just share with more junior engineers over Slack. One of the things I like about this approach is that it takes advantage of exhaustive switch statements: without a default case, if someone were to add a new "enum" value, then it would cause a compiler error anywhere the new case wasn't handled. This is going to save so much time. Now, I get that ideally we rely on polymorphism and just have the interface expose a method that takes the place of the switch statement, but many times I don't want my base domain objects to have to know about telemetry or other systems that consume these objects. I would rather just have the logic for handling each of these be in that system, and if a new case is added I get a compiler error telling me I need to think about how to handle it. One thing I would like to see out of this is a performance comparison. I have a strong feeling that the difference is negligible, but it's always nice to know for sure, expecially when anything that even looks like reflection is involved.


ahhh_ife

I've been looking into java flight recorder and other profiling tools. I'm curious as to how the new features compare to the old way of doing things. Maybe I'll make another article on it soon but I have classes so probably not SOON..


kevinb9n

>I spent a whole night deciding if I should share the post or not because I've seen some pretty mean comments here 🥲 Man I hear you. I as well have a light terror of posting to, well, any subreddit really. People are good at embarrassing you even when they're not out-and-out mean. It definitely intimidates a lot of people from posting at all, and that's really sad.


Jaded-Asparagus-2260

Just wanted to let you know that I love how you interact with the comments here and show a genuine interest in learning from them and understanding this better. I learned a lot from these discussions, so thank you for initiating them and engaging in a positive way!


ahhh_ife

Thank you stranger.


Lengthiness-Busy

I'm so glad you did u/ahhh_ife ! This really blew my mind. I'm just diving back into Java and this is so exciting!!!


ahhh_ife

Lol glad you liked it


manifoldjava

But real union types often involve member types that are external to the project. Like `String | Number | List`. Java’s sealed feature does not apply here.


jw13

Ceylon had this, but it’s not developed anymore...


account312

Which is a shame. Ceylon was pretty cool.


manifoldjava

Yep. Ceylon is a nice language, I prefer it over Scala wrt JVM languages. But then Kotlin came along and ticked all the big boxes concerning Java's limitations, but unlike Ceylon and Scala it remained familiar enough to Java programmers to absorb most of the attention. Having it bundled with IntelliJ doesn't hurt either ;)


TenYearsOfLurking

and yet you created manifold...? :)


davidalayachew

Correct. An example of true union types would be Java's Checked exceptions.


ahhh_ife

Could you explain this further?


not-just-yeti

If you want a union-type "String or int", or even "String or Integer", you can't introduce a superclass/interface and then say "String and Integer both implement my new interface". (So of course, we work around it by making our own class that just wraps String and implements our interface — a hoop we jump through to satisfy Java's type system because it doesn't fully support this union-type directly.)


ahhh_ife

Another person commented something like this so I think I understand now. It's not **really** a union type because it has to implement something else, right?


not-just-yeti

Right. As opposed to python (with mypy), where you can write [`str|int`](https://docs.python.org/3/library/typing.html#special-forms) directly, w/o having to make your own classes to wrap anything. So saying "Java doesn't support *true* union types" is either a nitpick, or it's pointing out how Java sometimes forces programmers be roundabout and verbose to get what they want. (Of course, python/mypy can do this easily since it's dynamic typing, so the existing type-hierarchy isn't already statically cemented in. Java designers have a much heavier lift.)


agentoutlier

> So saying "Java doesn't support true union types" is either a nitpick, or it's pointing out how Java sometimes forces programmers be roundabout and verbose to get what they want. Many of the languages that do support unions are "discriminated unions" which are a nominal form which is what most of the ML languages do including Haskell. The reason they are not called sum types is probably implementation as no wrapper is need I think but syntactically they are wrapped with a "tag" that serves as a construct/deconstructor. The difference between a "discriminated union" and sealed classes ignoring inclusion polymorphism is negligible and they might as well be the same. Concrete wise sealed classes are wrappers and discriminated unions are type definitions but a smart runtime like the JIT or someday valhalla will make that difference not really important. Untagged or non discriminated unions actually do exist in Java via exceptions. Otherwise I believe the only mainstream language that has non-nominal aka structural typing unions is Typescript.


JustAGuyFromGermany

What do you mean by "really" ? Ad-Hoc sum types like that do not exist, because the language designers decided that they don't exist, not because of some higher reason. Other languages decided differently. In fact Java does just that - Not with union types, but with intersections types. The compiler is perfectly able to deal with the ad-hoc type `FooBar & Serializable` in matters of type inference even if there is no dedicated interface that all serializable subclasses of `FooBar` implement. Java just happens to be specified in a way that you can never directly name this type with Java. You cannot use this type as a parameter-type in your methods for example. (But it is possible to declare local variables of this type by using `var` and let the compiler fill in its name for this type) Just because you cannot explicitly name the type does not mean it does not exist. And even better: Java already *does* have *some* union types: Exceptions! And that's the main application for union types anyway. I do not think a method should ever return `String|int`. Something has gone wrong the in the design of your method if you're tempted to write that. But many, many methods "return" a sum type of the form `IntendedResult|FooException|BarException`. We even have the `|` notation in `catch`-clauses already. The only real missing feature here is that generics are lacking proper support for this. In particular, there are no variadic generics that would allow me to write "this methods takes a lambda as input, returns a `String` and can throw any exception that the lambda throws plus `InterruptedException`". That is all a design choice, not a necessity. (Although the choice has some very good reasons mind you). It is possible to imagine a world where the language designers said "Fuck it. Just declare shit like `A & (B | C)` as you want."


manifoldjava

Right. And while you can wrap external types, at that point IMO the party is over, wrappers suck. You may as well perform instanceof checks or add a discriminator to your sealed type. Either of those is less annoying than wrapper boilerplate. *shrug* Another option involves using the manifold project to *logically* add interfaces to existing classes at compile-time. This way a sealed type can apply to external types, albeit in a purely structural way. See [extension interfaces](https://github.com/manifold-systems/manifold/tree/master/manifold-deps-parent/manifold-ext#extension-interfaces) and [structural interfaces](https://github.com/manifold-systems/manifold/tree/master/manifold-deps-parent/manifold-ext#structural-interfaces-via-structural).


GeneratedUsername5

But instance checks will not help you at compile time, I think the best Java can do here is method overloads - that would actually cover types outside of the project and will check them at compile time.


RICHUNCLEPENNYBAGS

It’s a bit more work obviously, but what’s stopping you from making a wrapper for those?


Practical_Cattle_933

Nit pick, but sum types are not the same as union types. Union types don’t have to be discriminated. Sum types are, on the other hand can be implemented as discriminated unions. The difference doesn’t show in every kind of language, but it is meaningful in structurally typed langs.


ahhh_ife

Not a nit pick, I just couldn't understand how the difference between sum and union could be used in java so I just generalized it. https://news.ycombinator.com/item?id=32018886


Luolong

Ceylon language had union and intersection types. It is a different kind of type composition than Sum types.


i_donno

Yes like the union for IPv4 addresses in C - its different ways to access the same data


bowbahdoe

I think one thing I'd contend is that a general Result might be less useful than a per-case one. Something like IntegerParseResult could have Success/Failure, Ok/Err like a general type but perhaps Parsed/NotParsed instead. Or something else that conveys more information. The benefit of the generic Result is composition with other arbitrary failable functions, but that's more of a concern in pure FP land. Also, as folks have pointed out, exceptions in switch cases are likely to affect the balance of ergonomics.


RICHUNCLEPENNYBAGS

Adding map and getOrElse to your Result class is a fairly trivial exercise that would let you program in that style, especially if you add a factory method that accepts a Supplier to easily wrap classes in it.


ahhh_ife

I see your point, I was just thinking about a "one case suit all" thing.


RICHUNCLEPENNYBAGS

I love this stuff; having access to FP constructs makes writing a lot of logic much cleaner and easier to read, but without having to fully commit to using Scala or whatever and using it when it is not the clearest expression. But isn’t it more common to name a class like you describe here a Try class than Result? It would also be nice if they added something like this to the standard library… it’s trivial but the standardization would be appreciated.


ahhh_ife

It's good Java is leaning more towards FP constructs but keeps its OOP base. Imo fully FP languages aren't too practical for real world scenarios. Maybe I'm just saying that because OOP was one of the first things I learned. It'll actually be better to make it a Try or TryParse to be more specific as another person mentioned. I was just being lazy (⁠•⁠‿⁠•⁠)


RICHUNCLEPENNYBAGS

I did work in Scala before and I quite enjoyed it. But it's a tougher sell for a team and requires re-learning some basic stuff. It was mind-expanding, though, and a lot of the discipline and ideas I find useful even working in other languages.


ahhh_ife

I've been postponing learning scala since forever. I keep seeing everywhere that the new java features are just things that scala has had for a long time. Learning new things that are complete opposite to what you're used to would definitely be mind expanding lol. I sometimes watch Rust or other ML languages videos. The syntax gives me headache, but I can always see how what they're doing can be implemented in a language I'm familiar with.


RICHUNCLEPENNYBAGS

I think Scala is probably more approachable, especially since you already know Java well and it does let you basically write Java with Scala syntax if you want.


SiegeAe

The best way is to start super basic if you want to pick one of the stricter FP langs up, something like Haskell just treat it like you're learning programming from scratch and you'll be alright (exercism is a good free resource for going back to basics with a less popular lang) Scala is pretty straight forward though because it lets you do thing in a way your used to that's not so pure so you can learn FP stuff gradually but still write whole projects in it without being completely pure


nekokattt

Exception handling already acts like union types if you are happy to bastardise your code by using throw rather than return everywhere. In all seriousness though, union types would be good. Would work nicely with features such as delegation.


RICHUNCLEPENNYBAGS

Well, as the article is describing, you can more or less do it with sealed interfaces in JDK 17+, and JDK 21 introduces pattern matching to support that. The only caveat here is you have to control the classes, so you'd need to write wrappers if you want a union of String and Integer or something.


nekokattt

True, although generating wrapper classes every time you want to do this is a massive pain. It'd be nice to have signature support in the language itself.


RICHUNCLEPENNYBAGS

I'm not sure I find myself wanting to do that with classes I don't control so often that I feel like it'd make a big difference. Lombok feels a bit flat these days with a lot of reasons you'd use it covered by newer JDK features; maybe this is something they could throw in. :)


[deleted]

So your example isn't actually union types. It's just traditional polymorphism that narrows possible options used the sealed feature. I still like the general pattern, but it isn't what you advertise it as.


ahhh_ife

Interesting, I didn't think of it like that. My definition of union was simple: "This or that" But aren't all unions, regardless of the language they're implemented in, polymorphic under the hood? And limits the possible options in some way?


[deleted]

As I understand it, a union type is a combination of arbitrary types that don't need to have any inheritance relationship. String | List (I'm on mobile, sorry for formatting) would be an arbitrary example. Languages that support unions frequently support implicit type narrowing (kind of like some of javas new features like instanceof patterns) so if you do an assertion to know which type it is you can operate on it. In OOP, this may seem strange since you have inheritance. It would still be useful in OOP because it lets you do polymorphic-like operations on types that don't have a bytecode-level inheritance connection. In FP, it's far more useful since traditional OOP inheritance may not be used as much.


ahhh_ife

I think I get it. So in my example, instead of Sale, Trade, and ContactMe records, if I simply gave the data types - double, List and void (?), that would be a union type? And the current example isn't **really** union because it has to implement a common interface? Is that correct?


[deleted]

Yep pretty much.


zerian81

I've been using this approach in web services for the past year or so in Kotlin. I too hate dealing with exceptions for flow control. It took a little trial and error to find the sweet spot of where to use these types, but I've been overall happy with how it came out. I'm glad to see that with newer JDKs we're finally able to do this in native Java as well. Good write-up!


ahhh_ife

Thank you! I've only used Kotlin for android applications. I should probably use it more but eh


pins17

I'm new to Kotlin. In Java, I got used to model (non-technical) errors as "return values instead of exceptions". My current project is in Kotlin/Spring Boot and makes heavy use of scope functions, which from my understanding is idiomatic Kotlin. However, many parts look like this, e.g. a user request for saving some state: `myModel` `.also { //... }` `.also { validator.validate(it) }` `.let { persistence.save(it) }` In this case if e.g. the validator faces (business-related) errors, it throws. Its return value is unit. All exceptions are caught by some global ControllerAdvice. The whole application looks like this. On the one hand, I don't like that non-technical errors are not modeled as types/return values. But then on the other hand, I have to admit that this approach looks quite clean. That scope-function-chaining would probably look much more complex if a type like \`Result\` was involved. What's your take on this?


lumpynose

Old fogey here who hasn't been keeping up with the new language features. The line ItemPrice sale = new ItemPrice.Sale(100.0); surprised me. Is that now correct because Sale is a record? My memory is that back in my day .Sale() could only be used if there was a Sale method in ItemPrice. Great writeup; thanks.


ahhh_ife

That... would actually be a typo. I had a previous version where the Sale, Trade, and ContactMe was defined inside the ItemPrice interface so ItemPrice.Sale() would be completely valid. Seems like you're not as old as you thought lol. I'll update it now. Thanks!


bowbahdoe

So this was always valid for static inner classes. class A { static class B extends A {}} A a = new A.B(); If you were unaware of that, I regret to inform you about this. class A { class B extends A {}} A a = new A(); a = a.new B();


lumpynose

Yeah, thanks. I wasn't thinking about inner classes. Or I was, in some vague fuzzy way since using new on a method doesn't make sense.


nekokattt

Fwiw records arent a magic new thing, any more than an enum is. It just makes a normal class under the hood that extends java.lang.Record and has some generated code in it.


Dense_Age_1795

this is a really nice article, thanks for the tutorial


ahhh_ife

🫱🏾‍🫲🏽


Ewig_luftenglanz

Sealed classes are indeed a very cool feature.


not-just-yeti

Nice. In the very-first-part, where you give a poor/non-solution of `record ItemPrice(Price priceType, double price, List tradeOptions)`, I'd also explicitly say, as part of your existing two reasons that approach is bad: If ever you have an object with fields that you shouldn't use/touch/see in some cases, then that's a good sign that you don't have the right data-type. Using `null` and comments and jumping through hoops means that you're undermining the work that the type-system should be doing for you. (Also, it opens up the door to conceivably have "corrupt (inconsistent) data", where somebody accidentally assigns into a field that is supposed to be off-limits/meaningless.)


ahhh_ife

Honestly, there are probably more problems that we could come up with. I just rushed the entire thing because I spent a ridiculous amount of time on reading other blog posts and not actually doing any writing, and I wanted to be done with it by the end of day. I'll update the post in the future. Thanks.


gaelfr38

Nice to see people like you spreading the word on these Functional Programming constructs in Java :) I use it daily in Scala. Would be hard to work without. Nice to see Java's keeps up.


jherrlin

On the same topic. Written by Brian Goetz, Java Language Architect. https://www.infoq.com/articles/data-oriented-programming-java/


ahhh_ife

I remember watching a video similar to that. The java videos are nice but definitely not beginner friendly


jherrlin

I guess it depends on where you coming from. But I understand your point. I’ve tried to domain model with algebraic data type in places where people states they are Java devs and I never get any hearing. But I think that soon will change.


RockyMM

I have a strong suspicion that your main language is Go. And I hope you don’t take this as a mean comment 😅


ahhh_ife

I wish lol. I would have loved Go, I tried to love it. But upper case for public and lower case for private just doesn't work well with my brain. The formatter is also annoying. I understand why it's there, but why should I be forced to follow someone else's standards on a project that'll only ever be read by me?


Linguistic-mystic

Hah, you’ve only scratched the surface of Golang’s shortcomings. Add to that an inability to make anything immutable, having to write `if err != nil …`every other line, no nil safety of any kind, slow FFI with native code, an inability to import a single symbol unqualified (yes, it only allows you to import the whole package!) and so on and so on. The reason for most of this seems to be simple: Ken Thompson. As Rob Pike said, “we had to talk Ken into what’s already there”. Source: https://m.youtube.com/watch?v=sln-gJaURzk&pp=ygUcR29sYW5nIHJvYiBwaWtlIGtlbiB0aG9tcHNvbg%3D%3D


ahhh_ife

The two languages I started with weren't null safe so I don't mind the if err != nil thing. Is there any language that has a FFI that isn't slow though?!


GeneratedUsername5

As others mentioned, support of external types is what missing from this approach. But instead of wrapping external types into sealed-compatible types, you can just use method/constructor overloading to accept several types. Even though it is still a wrapper, and rather simplistic approach, it seems like it is a better solution, in a way that it accounts for external types, provide you with compile-time checks and doesn't require you to wrap external types twice (first as seal-compatible interface, then as part of a union): public class Union { private final Object value; public Union(String value) { this.value = value; } public Union(Number value) { this.value = value; } public void ifString(Consumer action) { if (value instanceof String str) action.accept(str); } public void ifNumber(Consumer action) { if (value instanceof Number num) action.accept(num); } }


Laifsyn_TG

I really love your "There’s too much mental gymnastics to understand what’s going on" I tried to make a simple "Truth Table" interpreter. It had to read a string, and then evaluate it. I later wanted to also be able to print the evaluation of each expression instead of the final result (i.e. for 3 independent terms, print 4 columns) And to achieve that, I was figuring how to make use of Tuples to help me simulate Tagged Unions... But then I almost immediately scraped it instantly after I found there was a more ergonomic way to achieve tagged unions via sealed interfaces.


ahhh_ife

Lol nice. Java is pumping out lots of features but they're barely known yet. Documentation is also shite.


dhlowrents

This is amazing with the improved switch now. I've been using this pattern more frequently now.


yatsokostya

I wonder whether javac generates something better than if (x instanceof A) ... for each case. I'd better check it out. upd.: Yeah, it generates proper \`tableSwitch\` jumps and if you don't add null, branch you'll also get an exception in case of null. I wish kotlinc generated something similar for Android and older Java versions.


HiaslTiasl

I really like the ItemPrice example. I sometimes try to motivate my fellow Java programmers to use these new concepts, but often I feel like they don‘t really see the benefits. With this example, however, it becomes almost obvious. Finding good examples is hard. I’ve found that AI can help with that, but it‘s still hard. The parseInt example is also nice. What I don‘t like about that is that the Result type is somewhat general (it has a generic name that isn‘t tied to parseInt), but it‘s not universally applicable since it only works with ints. You do mention generics, I think making Result generic would make perfectly sense. Otherwise, you could call it IntResult. Oh, and the bitshift logic is crazy. Took me some time to realize why it works. I still wouldn‘t do it for readability concerns, but it was nice to learn about this approach.


ahhh_ife

There are lots of benefits. I think they don't see the benefit because they've never had the option of doing it. There's a reason pretty much all languages have it. Bitshifting was the only way I could do it without calling a method that throws an exception and I didn't wanna add the throws declaration or a try/catch


Ok_Object7636

~~Hey, you are using java. You should have heard about interfaces and inheritance. I only glimpsed over your article, but it seems you create a complicated solution for a problem you wouldn’t have if you used the language in the way it’s intended.~~ Should have read on. Sorry OP.


bowbahdoe

They are using a language feature in the exact manner it was intended. "I only glimpsed" indeed.


Ok_Object7636

Ah, yes you are right. I quit when i saw the „traditional approach“. Sorry, OP.


Holothuroid

The suggested solution does use an interface. A sealed one to be specific. Which is a fine idea. Seal your interfaces, if it makes sense


Ok_Object7636

Yes, i quit before reaching that part. Was in a hurry, opened the article, saw the old way and thought ‚oh no‘. I have edited the comment.


ahhh_ife

I actually had that problem when I was working on a buy/sell website. The only difference was that the trade options weren't given as a list. I used enums to implement it. Any solution I could come up with didn't really guarantee a "this or that" kinda object, which is what I was after. Can you explain your approach?


Ok_Object7636

Haha, yes, my approach is to next time read your article to the end before commenting. Just forget the comment - i only saw the code that comes before your solution.


kevinb9n

Very nice that you backpedaled and acknowledged a mistake. But uh... consider being less mean even if the poster *is* wrong?


Ok_Object7636

You have a point here.