Bikeshedding alarm: Could rethrows be replaced through throws?(E)?
public enum MyLibraryError: Error {
case errorProcessingHashedData(baseError: Error)
}
public func withHashedData<ReturnValue>(
_ input: Data, _ body: (SHA256Digest) throws -> ReturnValue
) throws?(MyLibraryError) -> ReturnValue {
let hashed = SHA256.hash(data: input)
do {
return try body(hashed)
} catch {
throw MyLibraryError.errorProcessingHashedData(baseError: error)
}
}
It would preserve the throwing if the closure thrower, but would expose the custom error directly. If the closure is non throwing, it won't throw its own type.
In other words this is basically a typed rethrows.
I really like that we go into interesting detail discussions, but just to give this discussion direction and structure, can we first clear up some determining factors?
Can someone explain to me, what changes to the language are in general allowed at all at this stage of the language?
Is source compatibility required no matter what? If this is the case, we will have a hard time with this proposal I guess. Concentrate on section "Source Compatibility" first as I already mentioned.
Is ABI stability required no matter what?
To me this is important, because it determines the options we have at all and we can fail on nasty details fast here without needing a discussion for the other things (though I really like the ideas you come up with ).
Is source compatibility required no matter what? If this is the case, we will have a hard time with this proposal I guess.
Source compatibility is the first of those principles, so I agree with your conclusion. This pitch won't easily turn into a proposal.
Is ABI stability required no matter what?
Of course it is. You can't break existing devices. But you can evolve the language so that typed throws is only available from a specific Swift version and specific system version (compare with some View which is only available on some version of the systems that rely on ABI stability).
This was my conclusion along the last days writing on the pitch. Bringing more information into the error types making them more specific in combination with type inference leads to a situation where error types change automatically in existing code (getting more specific). So as long as we are not auto converting source code (which I guess is not an option anymore) to explicitly declare the existing errors to be of type Swift.Error, I see no way to bring this into the language.
Well, start to see what happens when you do not automatically change error types in existing source code?
Maybe you'll end up with a perfectly viable language with useful new features, and that will be cool. Maybe you'll end up in a dead-end, but the community will have a better sense of what's to fix.
Anyway, the goal is not to tame the language so that it is like you want it to be. There are many flavors of "typed throws", and the game is to find the one that suits Swift the best.
And that's the whole purpose of this thread. There're some ideal cases where typed throws could be applied into the language and be useful, but the break source compat. The base idea we're proposing does not break source compat at first analysis, but it is more limited about what could it do breaking it.
I'm eager to see more ideas and helping us polishing the raw edges of the pitch to make it into a proposal soon.
Having some help on implementing the basic behavior (syntax) it would be very useful for us to see what can and what can't be done. Where should I start? On swift-syntax?
I wan't to reduce potential risks as early as possible. So to be concrete and keep my motivation going, I have no solution keeping source compatibility for this
struct Foo: Error { ... }
struct Bar: Error { ... }
var throwers = [{ throw Foo() }] // Inferred as `Array<() throws -> ()>`, or `Array<() throws Foo -> ()>`?
throwers.append({ throw Bar() }) // Compiles today, error if we infer `throws Foo`
For me, I think I told you so. throws functions should never be inferred to be the typed throw but the base throws in collections (array, set), unless the type is specified as follows:
Now we want the same but with typed Errors as well. So we have the following additional cases:
3 (typed version of (1)):
func foo<E>(_ bar: () throws E -> ()) rethrows E {
try bar()
}
// If every function implicitly threw Never, we could write
func foo<E>(_ bar: () throws E -> ()) throws E {
try bar()
}
// which would semantically be the same
4 (typed version of (2)):
func foo<E>(_ bar: () throws E -> ()) rethrows CustomError {
do {
try bar()
} catch {
throw CustomError(base: error)
}
}
I cannot think of a reason ATM, why this should not work.
I think it would be feasible to have a rule of the form:
func foo<T>(_: () throws T -> ()) { ... }
let _ = { throw Foo.error } // inferred as '() throws -> ()'
foo({ throws Foo.error }) // inferred as '() throws Foo -> ()'
That is, when there's no context we default to Error, but if there's a generic parameter to bind the throws type to, we allow more specific inference.
It also seems fairly likely to me that the source break wouldn't be that large (unless there's a more common use-case I haven't thought of?). It might make sense to tighten up the inference behavior as a change in Swift 6, where the Swift 5 compatibility mode would continue to infer the throws type as Error.
do {
throw Foo.error // statement infererred as Error instead of Foo?
} catch let error as Foo {
// would that catch?
}
// can we still check exhaustiveness?
How is catch working internally? Does it need the inferred specific type at this point?
To make it more accessible for everyone on the forum to participate in the discussion, we try to provide an up-to-date excerpt of the discussion going on targeting the main topics we see so far. If you feel that something is missing, let us know. Remember it's an excerpt, if you are really into it, read the whole thread.