Remix.run Logo
redditor98654 a day ago

A while ago, I had attempted to do it the Rust style with a Result type in Java (where it is called Data oriented programming) and the result is not great. I was fighting a losing battle in a naive attempt to replace checked exceptions but still retain compile time type safety.

https://www.reddit.com/r/java/s/AbjDEo6orq

lock1 a day ago | parent | next [-]

Interesting anecdote.

At $DAYJOB, I'm currently migrating a non-performance sensitive system to a functional style, which requires defining basic functional types like Option<> and Result<>. So far I think it works pretty well and certainly improves edge case handling through explicit type declaration.

My current stable incarnation is something like this:

  sealed interface Result<T,E> {
      record Ok<T,E>(T value) implements Result<T,E> {}
      record Error<T,E>(E error) implements Result<T,E> {}
  }
Some additional thoughts:

- Using only 1 type parameter in `Result<T>` & `Ok<T>` hides the possible failure state. `Result<Integer>` contain less information in type signature than `Integer throwing() throws CustomException`, leaving you fall back on catching `Exception` and handling them manually. Which kind of defeats the typechecking value of `Result<>`

- A Java developer might find it unusual to see `Ok<T,E>` and realize that the type `E` is never used in `Ok`. It's called "phantom types" in other language. While Java's generic prevents something like C++'s template specialization or due to type erasure, in this case, it helps the type system track which type is which.

- I would suggest removing the type constraint `E extends Exception`, mirroring Rust's Result<> & Haskell's Either. This restriction also prohibits using a sum type `E` for an error.

- In case you want to rethrow type `Result<?,E>` where `E extends CustomException`, maybe use a static function with an appropriate type constraint on `E`

  sealed class Result<T,E> {
      public static <E extends Exception> void throwIfAnyError(Result<?,E>... );
  }
- I like the fact that overriding a method with reference type args + functional interface args triggers an "ambiguous type method call" error if you try to use bare null. This behavior is pretty handy to ensure anti-null behavior on `Option.orElse(T)` & `Option.orElse(Supplier<T>)`. Leaving `Option.get()` as a way to "get the nullable value" and a "code smell indicator"
Defletter a day ago | parent | prev [-]

Yeah, Java's generic-type erasure makes result types difficult, though as you mention in your code example, this can be mitigated some using switch guards. But you could also go into another switch:

    switch (result) {
        case Ok(var value) -> println(value);
        case Err(var ex) -> switch (ex) {
            case HttpException httpEx -> {
                // do something like a retry
            }
            default -> {
                // if not, do something else
            }
        }
    }