Just to recap what some people with strong knowledge of the language thinks about this:
I agree that overly specific API specs are a concern. There's still some documentational benefit to a typed error enum, even if it ultimately ends up containing a catch-all case, since it gives you the ability to also enumerate some number of discrete, realistically handleable cases. Sure, your network request running in an XPC service has infinitely many failure cases due to resource constraints or IPC failure or network chaos, but there's usually some number of domain-specific error cases too. And if we compare the proposed "you get to specify one error enum type" model to, say, Java or C++98's "you get to specify any number of error classes" model, I think that helps steer people away from the most grievous API mistakes in Java land, since instead of listing a closed set of concrete error classes directly in your API signature, you'll list those error cases in your enum definition, and in a resilient world, the enum will be "open" by default to external users, preventing it from being a permanent liability unless the user explicitly opted into closedness.
In the discussions around Rust's error handling conventions, they recognized this pattern of APIs either raising some number of layer-appropriate errors or carrying forward the failure modes of the layers below them, and they developed a convention for errors wrapping other errors. It would be reasonable to say we should do the same thing as part of the typed errors design. If we were to generalize enum subtyping beyond Optional, that might be one way to go about it, letting an enum wrap its underlying layers' failure cases as subtypes.
-Joe
As you know, I still think that adding typed throws is the right thing to do. I understand your concern about “the feature could be misused” but the same thing is true about many other language features.
One thing you didn’t mention is that boxing thrown values in an existential requires allocation in the general case. This may be unacceptable for some classes of Swift application (in the embedded / deep systems space) or simply undesirable because of the performance implication.
-Chris
It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.
If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.
I don’t like any solution that is based on comments. I think that compiler should always ignore comments.
I agree. And in general, this sort of thing is exactly my core concern about adding typed throws to the language: I am completely certain that many programmers will add typed throws annotations because they're programmers and thus, well, probably a little obsessive/compulsive, and they're trying to precisely document the behavior of their function without necessarily thinking about the usefulness of that information for their clients and (if they're writing a library; and really you should almost always be writing code as if you're writing a library) whether they're actually willing to commit to that behavior in their interface. For those programmers, typed throws is just going to box them in and force them into anti-patterns in the long run.
In the vast majority of use-cases, clients are not going to exhaustively handle all errors — they will always have some generic fall-back. That is not pessimism, it's actually the natural result of the complicated world we live in, where code can fail for a huge host of reasons and most callers won't have meaningful special-case behavior for all of them. (On most operating systems, opening a file or a network connection can fail because you ran out of file descriptors. You're seriously telling me that you're going to add a special case to your error logic for that?) Go look at the actual error types that people use in most typed-throws situations and try to tell me I'm wrong — they probably have like twenty alternatives, and solidly a quarter or more of them will just be embedding some other arbitrarily-complex or stringly-typed error value.
The real use case for typed throws is when you have something like a parser library that really does only fail in a fixed number of semantically distinct ways, and you both (1) actually care about enforcing that in the implementation and making sure that other errors are handled internally and (2) you really do expect clients to exhaustively switch over the error at some point. That's important. But I continue to think that if adding better support for that use case misleads other programmers into thinking they should use typed throws, we will have made the language worse overall.
John.
+1
-Chris
···
On Feb 17, 2017, at 11:29 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org > wrote:
On Feb 17, 2017, at 1:24 PM, Xiaodi Wu via swift-evolution <swift-evolution@swift.org mailto:swift-evolution@swift.org > wrote:
Let's not bring bikeshedding the commonly proposed and rejected union spelling into this.
Typed throws would be a nice addition, assuming that the core team finds it in scope for phase 2. It seems only logical that any type can be thrown (i.e. conforms to Error) should be permitted to be listed in throws()
.
Agree. Typed throws should have a single thrown type. Making it more convenient to throw and catch more than one error type with typed throws without having to manually create a wrapper is an orthogonal issue. David Owens convinced me of this last year when we had a thread on the topic.
IMO, if we accept a single error type per function, there could be a simpler model for this. We could say that the throws
type is a generic parameter of all function types, and it defaults to the uninhabited Never
type for functions that don't throw.
() -> () == () throws Never -> ()
() throws -> () == () throws Error -> ()
In this model, you'd get many benefits:
rethrows
could become first-class, reducing down to just polymorphic throws
:
func foo(: () throws -> ()) rethrows // Swift 3
func foo<T: Error>( : () throws T -> ()) throws T // Swift X
func foo<T: Error>(_: () throws T -> ()) throws Either<MyErrors, T>
Protocols could abstract over error handling; for instance, we could support throwing sequences:
protocol IteratorProtocol {
associatedtype Element
associatedtype Error: Swift.Error = Never
mutating func next() throws Error -> Element?
}
There're strong opinions on the fact that typed throws are a thing to include into the language and that even it could be ABI backwards compatible using the introduced semantics. For record that someone has it here to read and have more context about this.
Also, I'd like to see more dos and don'ts with the proposal in code in order to see what issues the semantics would have so we can analyze edge cases and other maybe related topics.
All contributions and opinions are more than welcome!
Thank you.
Some topics to cover:
Generic throwing allowed
Protocol establishing Error type through associatedtype
.
Extensibility of errors.
ABI compatibility
Semantics
Different Error
do
statement.
6 Likes