The only difference between a `fun doThing: Result<X, SomeError>` and a `fun doThing: X throws SomeError` is that with the checked exception, unpacking of the result is mandatory.
You're still free to wrap the X or SomeError into a tuple after you get one or other other. There is no loss of type specificity. It is no harder to "write functional code" - anything that would go in the left() gets chained off the function call result, and anything that would go in the right() goes into the appropriate catch block.
I also don’t understand the argument that Result is anything other than a syntactic difference between these ideas.
vs. Both eat the error. Both wrap the value. The rust is more terse, but the meaning is the same.You've discarded the error type, which trivialised the example. Rust's error propagation keeps the error value (or converts it to the target type).
The difference is that Result is a value, which can be stored and operated on like any other value. Exceptions aren't, and need to be propagated separately. This is more apparent in generic code, which can work with Result without knowing it's a Result. For example, if you have a helper that calls a callback in parallel on every element of an array, the callback can return Result, and the parallel executor doesn't need to care (and returns you an array of results, which you can inspect however you want). OTOH with exceptions, the executor would need to catch the exception and store it somehow in the returned array.
Try:
I have rewritten the parent code to preserve the exception without semantic difference or loss of type safety.If there are multiple types of exception that can be thrown, the right can be a union type.
Either is another name for a Result type.
Either works, but now you have two ways of returning errors, and they aren't even mutually exclusive (Either-retuning function can still throw).
Catch-and-wrap doesn't compose in generic code. When every call may throw, it isn't returning its return type T, but actually an Either<T, Exception>, but you lack type system capable of reasoning about that explicitly. You get an incomplete return type, because the missing information is in function signatures and control flow, not the types they return. It's not immediately obvious how this breaks type systems if you keep throwing, but throwing stops being an option when you want to separate returning errors from the immediate act of changing control flow, like when you collect multiple results without stopping on the first error. Then you need a type for capturing the full result type of a call.
If you write a generic map() function that takes T and returns U, it composes well only if exceptions don't alter the types. If you map A->B, B->C, C->D, it trivially chains to A->D without exceptions. An identity function naturally gives you A->A mapping. This works with Results without special-casing them. It can handle int->Result, Result->int, Result->Result, it's all the same, universally. It works the same whether you map over a single element, or an array of elements.
But if every map callback could throw, then you don't get a clean T->U mapping, only T -> Either<U, Exception>. You don't have an identity function! You end up with Either<Either<Either<... when you chain them, unless you special-case collapsing of Eithers in your map function. The difference is that with Result, any transformation of Result<T, E1> to Result<U, E2> (or any other combo) is done inside the concrete functions, abstracted away from callers. But if a function call throws, the type change and transformation of the type is forced upon the caller. It can't be abstracted away from the caller. The map() needs to know about Either, and have a strategy for wrapping and unwrapping them.
catch lets you convert exceptions to values, and throw convert values to exceptions, so in the end you can make it work for any specific use-case, but it's just this extra clunky conversion step you have to keep doing, and you juggle between two competing designs that don't compose well. With Result, you have one way of returning errors that is more general, more composable, and doesn't have a second incomplete less flexible way to be converted to/from.
I think you're missing the key point about return types with checked exceptions.
`int thing()` in Java returns type `int`. `int thing() throws AnException` in Java returns type `int | AnException`, with language-mandated destructuring assignment with the `int` going into the normal return path and `AnException` going into a required `catch` block.
`int thing() throws AnException, OtherException` returns type `int | AnException | OtherException`.
The argument you're making, that the compiler doesn't know the return type and "you lack type system capable of reasoning about that explicitly", is false. Just because the function says its return type is `int` doesn't mean the compiler is unaware there are three possible returns, and also doesn't mean the programmer is unaware of that.
The argument you are making applies to UNchecked exceptions and does not apply to CHECKED exceptions.
It's not a single return type T that is a sum type. It's two control flow paths returning one type each, and that's a major difference, because the types and control flow are complected together in a way that poorly interacts with the type system.
It's not `fn(T) -> U` where U may be whatever it wants, including Ok|Exception in some cases. It's `fn(T) -> U throws E`, and the `U throws E` part is not a type on its own. It's part of the function signature, but lacks a directly corresponding type for U|E values. It's a separate not-a-type thing that doesn't exist as a value, but is an effect of control flow changes. It needs to be caught and converted to a real value with a nameable type before it can work like a value. Retuning Either<U, E> isn't the `U throws E` thing either. Java's special alternative way of returning either U or E is not a return type, but two control flow paths returning one type each.
Compiler is fully aware of what's happening, but it's not the same mechanism as Result. By focusing on "can this be done at all", you miss the whole point of Result achieving this in a more elegant way, with fewer special non-type things in the language. Being just a regular value with a real type, which simply works everywhere where values work without altering control flow is the main improvement of Result over checked exceptions. Removal of try/catch from the language is the advantage and simplification that Result brings.
Result proves that Java's special-case exception checking is duplicating work of type checking, which needlessly lives half outside of the realm of typed values. Java's checked exceptions could be removed from the language entirely, because it's just a duplicate redundant type checker, with less power and less generality than the type checker for values.