So you want to reduce:
catch let error as FooError { ... }
to
catch error as FooError { ... }
or even
catch FooError { ... }
So you want to reduce:
catch let error as FooError { ... }
to
catch error as FooError { ... }
or even
catch FooError { ... }
We have that covered: https://github.com/minuscorp/swift-typed-throws/blob/master/SE-XXX%20Typed%20throws.md#library-evolution
Something along that line. IMO unannotated catch
should be reserved for untyped handling. It is much easier to explain that way, and seems to lead to a more coherent system. Swift types are know to get very long, though.
A couple notes—in the section on generic throws
parameters, IMO it would be more consistent with Swift's existing generics system for
func foo<T>(_ block: () throws T -> Void) rethrows T
to be a compilation error, rather than raising the error at the call site (i.e, we should require the T: Error
constraint). E.g., if this were instead written as:
func foo<T>(_: T.Type, _ block: (() throws T -> Void)?) rethrows T
would it be an error to write:
foo(Int.self, nil)
?
Also, regarding this point:
f({ throw E.failure }) // closure gets inferred to be `() throws E -> Void` so this will compile fine
as I noted in a comment above, this inference of the throws
type is not, in general, source compatible. Is the intent that this inference only takes place in the context of a generic throws
type? If so, that should be made very explicit.
There's no way that T does not conform to Error because it is annotated after the throws
statement, so it is verbose IMO.
It is compatible because it is the block which is being inferred to be E
, we were talking about the inference inside the empty catch
clause. That function behaves as any other generic function where the type is inferred automatically.
correct, but this can be open for discussion when the pitch is finished next week
Is there precedent in the language for this sort of implicit generic constraint? My mental model for generics right now is that if I supply a concrete type which satisfies the generic constraints, the call will compile. I'm wary about breaking this model. E.g., it feels wrong to me that the following compiles just fine:
func foo<T>(_: T.Type, _: () throws T -> Void) rethrows T { ... } // Compiles fine
but we would get an error here:
func bar<T>(_: T.Type) {
foo(T.self, { }) // Error!
}
If you're limiting this only to contexts where you are inferring the throws
type for a generic throws T
parameter, then yes, this is source compatible (since there are no such throws T
declarations today, of course).
However, if this inference is meant to be more broad, it is not source compatible:
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`
To be clear, I mean that all throw
statements becomes typed, and unannotated catch
blocks never attempt to infer the error type and use only Error
.
For instance, Hashable
is inferred here:
func singleton<K, V>(_ key: K, _ value: V) -> [K: V] {
return [key: value]
}
print(singleton(1, 2))
(Personally,I think it's a C++'ism which would've better been left explicit.)
Nice! Looks like my intuition is already broken, then.
It seems reasonable that this behavior is carried over to typed throws
.
Just read the new proposal document. A function can list only one throw type. So if I had a function that channeled two closures, each with its own error type, what am I supposed to do? I would want to list both in the new function's list. I could hope that the two arguments have the same type, or one is a subtype of the other; otherwise, the new function would need to list either Error
or a general throws
.
Should you mention function sub-typing. A non-throwing function is a subtype with any throwing function (with the compatible parameters & return-type). A typed-throws function is a subtype of a general throwing function. Two typed-throws functions have a subtype relationship if the corresponding thrown types have a subtype relationship. (If we add support for multiple typed-throws, then the various superset rules apply.)
You either catch both errors in their corresponding catch clause and throw the convenient throwing type of the function or maybe typed function is not appropriate for that function.
This is not for every function. It has its use cases
IIRC that is mentioned near Library Evolution section
I don't have time to read this whole thread or read the draft in detail, but it looks like you haven't adopted the design where non-throwing functions are equivalent to throws Never
. I strongly suggest you consider going in this direction as it improves composability and addresses the complications around rethrows
. Here's an old thread that is contains some relevant discussion.
We never rejected that idea, but it seems more than an implementation detail to me. Also none of the authors encountered issues with rethrows for now, but thank you for the reference, I'll check it out.
Edit: I don't think we want to erase rethrows from the language rn, as it does not impact on the proposal, also, also I don't think we want to make everything throw typed as it does not solve some issues that we have, they add even more edge cases.
This isn't an implementation issue, it's much more than that. It's central to how typed throws fits into he type system.
I'm not suggesting you do that.
You don't need to do that at the syntax level, but in the analysis I've done it simplifies things at the semantic level to interpret the existing syntax in terms of throws Never
and throws Error
.
Then apologies for not understanding your point in first place. I'll take in consideration how does it fit in the type system, I trust your analysis, we'll add it to the proposal in the grammar section
You're right. It is a non-goal (in my opinion) for Swift's error handling to be as expressive as TypeScript or Java's error handling. Its only goal is to be great for Swift.
On a few other points, yes, closures (and function types in general) should support typed throws. rethrows
should not take a type, it should propagate the type from the closure to the callee. This would make sure that:
try {
... = try thing.map { foo($0) }
} catch let x {
}
infers x
to FooError
if foo
is a function that throws only FooError
.
-Chris
I agree w/@Chris_Lattner3 that rethrow should just match argument. It typed throws only if the argument is typed throw. The usage of rethrow
is only to propagate the throwing anyway.
Now there's some interesting capabilities when we include typed throws, we may be able to restrict the error type:
func foo<T: FooError>(_: () throws FooError -> ()) {...}
But it seems to be a very questionable feature.
Right now, a function can only specify one thrown type, So what does a function that takes multiple closure arguments which may have distinct thrown types do during rethrows
? It:
Error
)?I'm aiming towards [4].
This is what I would expect.
#1 is not possible - you can only specify one error type. I don't understand what you mean with #4.
Related to rethrows
, here's an example to consider:
func takesTwo<E, F>(_ e: () throws E -> Void, _ f: () throws F -> Void) throws E -> Void {
try e()
do {
try f()
} catch _ {
print("I'm swallowing f's error")
}
}
This works like rethrows
in that when E
is Never
the call to takesTwo
will not be a throwing call. It works better than rethrows
in that it is not throwing even when F
is Error
or some other error type (i.e. not Never
). This is an example of why it is important to make non-throwing functions equivalent to throws Never
.