OO languages with traditional (but somewhat saner) exception handling have been doing something similar since forever; excuse me for the Swift example again, but that’s the language I have to write most code these days in. So, here’s the deal: Swift has almost the same thing in the form of a
rethrows keyword which can be applied to higher-order functions, denoting that the higher order function may throw if and only if its function-typed parameter may throw. (Meanwhile, closures passed as an argument to such functions have their throwing/non-throwing nature type-inferred.)
I can tell you from experience that it is very annoying, regardless of whether you are the author or consumer of such higher order-functions:
- From the author’s perspective, you basically have to manually annotate every single higher-order function you write.
- But from the consumer’s perspective, if the author of a higher-order function forgot to annotate just one of them, then BOOM you now can’t call it with a throwing closure at all.
I think with anything like this, we are drifting more and more in the direction of “traditional” exception handling, which has the severe downside that if exception throwing is explicit in types, then it adds a completely superfluous dimension to function types. And if it’s not, it doesn’t do any good, it basically makes the language dynamically-typed in this regard, making it harder for both humans and the compiler to reason about all code.
The only change that this
try-polymorphism will cause apart from enabling one to type one less
Ok and one less
? (which is hardly an issue currently anyway), is that now everyone will have to remember to make their functions “
try-generic” for absolutely no other benefit. I still don’t think that sometimes having to type
.map(Ok) is a problem, and having to type
? everywhere a
Result-typed, early-returning call is made is downright an advantage. As an aside, I write a lot of
Result-heavy code, and these sort of issues come up very rarely, and if they do, they are trivial to solve with existing language and library features.
So, back to the topic, one of the core strengths in Rust’s
Result- and convention-based error handling is that it doesn’t require a completely redundant dimension to the type system. There are no
throwing or non-
throwing functions; there are just functions that return a
Result, which is a regular type, with several useful combinators. Don’t forget that in the example you cited, all parts of the code have a meaning and a purpose: to ensure type safety. This would no longer be the case with the “
try-polymorphic” approach: increasingly more things would now be implicit and magical, and I don’t think that’s a very good direction for Rust to go in.