Using magic language syntax like checked exceptions in lieu of type system expressiveness is not great, which is why Java's checked exceptions are a bit on the unpleasant side. You can't have one map() that handles functions with and without exceptions, because exceptions aren't part of the type system.
In Rust, exceptions are part of the type system, so you only need one map(). If you also add syntactic sugar for the types such as:
1: fn foo(i: i32) -> string throws IOException
2: throw SomeIOException
3: try a catch IOException b
To mean, respectively and approximately: 1: fn foo(i: i32) -> Result<string, IOException>
2: return Err(SomeIOException)
3: match a {
None => b
Some(_) => {}
}
... you can keep the simpler program code of exceptions and keep the ability to express exceptions first class in the type system.The Rust implementation (syntax I use might vary from reality) of async/await is just this sort of syntactic sugar over a type: "async fn foo() -> X" is sugar for "fn foo() -> Future<X>", and "await!foo()" is (kind of) sugar for "foo().then(rest of the function)".
The only really kind of disappointing thing about this is that exceptions and futures don't get unified: we get ? for exceptions and await! for futures. Both of them ultimately are continuations: exceptions bypass the continuation and return an error immediately, futures call the continuation only when their value becomes available. The value of using a unified interface to continuations (or monads, if you prefer) is that you can use ones that don't have a magic blessed syntax: parsers with automatic backtracking written as simple imperative blocks, non-determinism via iterators where the continuation is a flatten operation, etc.
I don't want to have to worry about whether the function "throws" or not at the point where I'm calling map - that's the whole problem of doing this in Java. What if the function that calls map is itself a generic higher-order function? Result's great benefit over exceptions is that it isn't a special case; generic functions work with Result just as they work with string.
> In Rust, exceptions are part of the type system, so you only need one map().
So what's the answer to the question? If there's only one map() I should be able to use it to map with foo; when I do, what do I get back?
> The Rust implementation (syntax I use might vary from reality) of async/await is just this sort of syntactic sugar over a type: "async fn foo() -> X" is sugar for "fn foo() -> Future<X>", and "await!foo()" is (kind of) sugar for "foo().then(rest of the function)".
Sure, and try!foo() does a pretty similar thing for Result. I think that's a better approach than exceptions, because you can see what's going on at the call site - having functions that "throw" and don't "throw" look exactly the same at the point of use is too magic/confusing IME.
> The value of using a unified interface to continuations (or monads, if you prefer) is that you can use ones that don't have a magic blessed syntax: parsers with automatic backtracking written as simple imperative blocks, non-determinism via iterators where the continuation is a flatten operation, etc.
Yeah, I find continuations too confusing to reason about but I really wish Rust would adopt some general-purpose syntax for monads ("do notation"). That would require HKT though, because you can't even form the type of a monad in the general case without that.
If you're interested in which item failed, ideally you'd be using a method that doesn't throw at all, and instead returns an error condition. I like how C# makes this clear, with Parse and TryParse on Int32. You use Parse when you want unwinding and abort behaviour including stack unwinding, and TryParse when you want to handle errors, and you don't use exceptions for expected errors, since expected errors are not exceptional.
(If the map is lazy, you'd expect the materialization in collect() to throw. But the return type wouldn't change.)
IMO, the kind of code you write on a daily basis is the primary determinant of your preference for error codes vs exceptions. If you need to handle error conditions frequently, you'll prefer error codes, because they're data, like all other data, and you can use the general compositional tools at your disposal to manipulate them. If you handle error conditions exceedingly rarely, and mostly just want to abort, unwind, roll back, go back to the main loop and log them, then exceptions are your friend.
I do not think there is a fundamental superiority either way. I think there are tools for purposes. I do think that Java's checked exceptions are half-baked; they're half-way into the compile time type system in a language whose applications are generally better suited to run time exceptions.
You'd have to use unchecked exceptions to achieve that though. There's no way to propagate the checked exception across that call, because there's no way to make map() generic over exception-or-not in a Java/Rust-like type system.
> If you're interested in which item failed, ideally you'd be using a method that doesn't throw at all, and instead returns an error condition. I like how C# makes this clear, with Parse and TryParse on Int32. You use Parse when you want unwinding and abort behaviour including stack unwinding, and TryParse when you want to handle errors, and you don't use exceptions for expected errors, since expected errors are not exceptional.
Result gets you that without needing to write two implementations of every method. You call unwrap-or-panic in the cases where error is not expected/handled, and handle it in the cases where you want to handle it.