TL; DR rethrowing protocol conformances look like they give us a way out of the problems described here without going to a full typed-throws model.
Not speaking for the core team here, just myself. It seems likely, but the design of typed throws is not trivial and we potentially have to deal with more fundamental issues before the feature makes sense. For example, typed throws makes it far, far too easy to overconstrain APIs in a manner that limits their evolution. Can we do something better? Also, typed throws immediately runs into the need/desire for structural union types, which themselves require a lot of design.
However, I think this question is somewhat beside the point. With Result
, and now with the concurrency APIs, we're embracing the notion of a parameter to describe failure. Even if we never get typed throws, having the ability to generalize over the Failure == Never
and Failure == Error
cases is useful. So, I disagree with part of your comment here:
I agree that this could be a mistake that prevents future evolution. However, I don't necessarily agree that you need typed throws to solve the problem. Rethrowing protocol conformances capture the notion of throwing-ness of a particular conformance. Rethrowing protocol conformances are how you can
for await x in someAsyncSequence { ... }
when iteration through someAsyncSequence
doesn't actually throw, and you add the try
keyword in there when iteration through that sequence can throw.
I think the actual problem here is that, while the compiler keeps track of whether a conformance to a rethrows protocol is throwing or not, it doesn't expose that information in a manner that's useful for building on top of it. For example, let's imagine that AsyncIteratorProtocol
looked like this:
@rethrows public protocol AsyncIteratorProtocol {
associatedtype Element
associatedtype Failure: Error = /*Never if the conforming type's next() is non-throwing, Error otherwise */
mutating func next() async throws -> Element?
}
So, there's a little magic there in the way the type is inferred, because here's what we'd end up with:
struct MyNonThrowingIterator: AsyncIteratorProtocol {
// infer Element = Int, Failure = Never
mutating func next() async -> Int? { ... }
}
struct MyThrowingIterator: AsyncIteratorProtocol {
// infer String = Int, Failure = Error
mutating func next() async throws -> String? { ... }
}
I think that handles all of the cases you describe above, and it's built mostly on logic that already exists in the compiler. If we were to get typed throws in the future, AsyncIteratorProtocol
could evolve to something like this:
@rethrows public protocol AsyncIteratorProtocol {
associatedtype Element
associatedtype Failure: Error
mutating func next() async throws(Failure) -> Element?
}
I'm fairly certain we can make that evolution both source- and ABI-compatible, because generic clients have to deal with an arbitrary Failure
anyway, and the rule for inferring an associated type from a typed throws will have to infer Never
for non-throwing witnesses and Error
for witnesses that use throws
.
This is a fairly drastic measure. Again, I'm not speaking for the whole Core Team here, but I think we have better options than to rip out an important part of the concurrency model. For reference, an associated type with a default can be added to a protocol without breaking ABI. So even if we had today's definition of AsyncIteratorProtocol
, we could extend it later [*]:
@rethrows public protocol AsyncIteratorProtocol {
associatedtype Element
@available(Swift 5.6 or whatever)
associatedtype Failure: Error = /*Never if the conforming type's next() is non-throwing, Error otherwise */
mutating func next() async throws -> Element?
}
I think what's happening here is that you're comparing an implemented and therefore fully-understood design for rethrowing protocol conformances to an idealized view of typed throws that has yet to meet the reality of complete specification. For typed throws to solve the problems described here you need similar machinery to that of rethrowing protocol conformances, gathering up all of the potential throwing witnesses (and the types they throw!). However, instead of a simple yes/no answer, you have to consider how to combine multiple different results from different throwing witnesses, and all of the tricky semantic questions around typed throws crop up again.
I've very glad that @DevAndArtist brought this up. It's important, and I think we have reasonable solutions within reach.
Doug
[*] There is one bit of metadata we'll need to record for rethrowing protocol conformances to make the defaulting work properly. It's not a big deal.