SE-0413: Typed throws

Makes sense that you wouldn't want to slow down the "happy path" of code running without throwing! I'm curious about the implementation of opaque result types now:

Within a module, the specific concrete type is known (unless there's dynamic dispatch involved) so there is no overhead vs stating the concrete type. But across module boundaries, where I was assuming the same would hold, I take it they are actually closer to existentials instead? So that when a library changes the underlying type, its binary releases remain compatible with clients, I guess?

It’s more like a type parameter than an existential. We know it’s a specific type, so we can work with that information; we just don’t know what that type is statically, so the information is dynamic. Essentially, there’s a function that returns an Any.Type that we have to call, plus other functions to get all the protocol conformances that we know about.

2 Likes

On the other hand, because it's a fixed type, we generally eliminate that dynamic overhead when the compiler has visibility into the function definition and can see the concrete type underlying the opaque type (which, given the current restrictions on embedded Swift, should be all of the time). Using some Error to handle error propagation is appealing, but without being able to fall back to any Error it seems like, within the rules of the proposal as it stands, it would be ergonomically difficult to work with. Since every function's opaque return type is treated as a different type, you wouldn't be able to call two different throws(some Error) functions in the same typed catch block, or invoke two different throwing functions letting their errors propagate, when any Error isn't available to generalize over the different-looking opaque types. It also isn't clear how you would do this propagation manually, since callers wouldn't be able to either exhaustively switch over the opaque type or manually write a non-generic enum that can hold the heterogeneous thrown errors. Using a generic Either-like enum to propagate would in turn mean that the caller's own clients then need to catch and match that enum's cases to get at the underlying errors, meaning the propagation is no longer transparent.

These are probably surmountable issues, but I think we'd need a bit more language work to make some Error a viable solution for embedded Swift. I'm not going to ask this already-heavy proposal to take on yet more design work, but some possible follow up features to improve the ergonomics might be to allow for limited propagation of opaque underlying types within a module, and/or to allow for an enum to become a subtype of its payload types so that it can be transparently pattern-matched through like an existential.

4 Likes

It might be a bigger issue than typed do/catch blocks… as the proposal is written, all do blocks have an error type, which is elevated to any Error if the try statements throw different error types.

When throw sites within the `do` block throw different (non-`Never`) error types, the inferred error type is `any Error`. For example:
do /*infers throws(any Error)*/ {
  try callCat() // throws CatError
  try callKids() // throw KidError
} catch {
  // implicit 'error' variable has type 'any Error'
}

In essence, when there are multiple possible thrown error types, we immediately resolve to the untyped equivalent of any Error.

Even if the different error types are covered by individual typed catch blocks, will this cause problems for the type system while evaluating the do block?

This proposal has been accepted. Thanks as always for your detailed feedback.

– Steve

10 Likes