I think the idea in general seems promising, but none of the examples really stand out as being significantly improved by the feature. This one especially, the best you could really do is throw a general Error here, so the single improvement would be that you can make do with three catch clauses instead of for - no need for the catch-all.
And even this limited improvement with multi-error type cases only works "locally", callers of callFamily wouldn't have any benefits at all.
The more I read this pith the more it seems to me that typed throws only make sense when there is a simple and partly automatic way to construct and handle combined types, union types if you will, as we discussed:
If callFamily could throw something like KidError β SpouseError β CatError and this would be automatic, then it would make sense. If you need to catch and wrap each error type separately then it seems worse than untyped errors to me.
I don't see how that is hard to reason about... I see only one reasonable answer, is that the code should't compile. You could catch the errors and store them in an enum and throw that. Or handle the errors right away. Or catch the errors and do something else.
For me this is too magical. Swift is always very explicit, about which values a generic can possibly represent. IMHO, what is proposed, feels the same as:
foo<T>(a: T, b: T) -> Bool {
a > b
// T is implicitly constrained to Comparable,
// because it's used with a comparison operator
}
Yeah, the code as written above definitely should not compile, the point was just that it's a pretty big blow to composability if every usage of non-Error throwing functions requires the user to define an enum. We'd basically have a class of throwing functions which force client functions to handle all errors explicitly rather than propagating the errors to the caller.
I suppose you could create a generic enumeration to wrap an Error or Any error that could be reused across different functions, but then what have we really gained? I suspect it will be very surprising to beginners if:
does not compile, and even as a proficient Swift programmer I can foresee myself getting frustrated by this if non-Error throwing functions became a common pattern. Error is a super lightweight protocol, it's not difficult to provide a conformance for any given type.
FWIW, I had the same reaction in the discussion thread until the [T: U] case that @Lantua was pointed out (which isn't specific to Dictionaryβany constraints from the types specified in the signature will be implicitly inherited). The fact that the implicit constraints are limited to the signature of the function is restrictive enough to not upset me.
Oh okay...good to know. If it's allowed there, then it should be allowed here as well, I guess.
Still seems too magical to me, in the dictionary case as well. Nvm.
I'd like to see the implications of rethrows propagating the typed error with source compatibility or even with the scope of this proposal, maybe should be a follow up for this one, refactoring all throws seems more than enough for this proposal, don't you think?
Maybe @John_McCall can give some insight about this one. I'm certainly curious if it would be easier for the whole proposal to make this syntax analysis.
It's a nice, simple formalization of some of the type-system behavior, but it doesn't actually change the key question with rethrows, which is whether the signature of A below is equivalent to that of B or that of C:
// A
func foo(fn: () throws -> Void) rethrows
// B
func foo<erased E: Error>(fn: () throws E -> Void) rethrows E
// C
func foo<erased E: Error>(fn: () throws E -> Void) rethrows Error
B is the more aggressive answer, and it would generally allow callers of higher-order operations like && to maintain more precise error types. However, C is the current rule. If you assume rule B when calling a rethrows function written as A and compiled under rule C, the type system will be unsound, and if you enforce rule B instead of rule C when compiling a rethrows function written as A, you may reject code that was previously accepted.
You may also have noticed the erased that I added in the signatures above. Without it, neither of these interpretations is actually ABI-compatible with the current interpretation of A, because A does not actually take a concrete type argument E.
Should this change (B answer) be viable for the proposal? Seems like of we go for answer C many of the main sense of the proposal is gone. Also, would be source breaking? If the current code is not using a typed throw, it should behave as of today, it will change if you add the type in the throw, doesn't it?
It's nice when new features are "additive" that way, but no, rule B would place restrictions on rethrows functions regardless of whether typed throws is in use anywhere in the program.
Why is C the current rule? Does a function which rethrows allow you to throw your own type other than what fn could potentially throw? Is there a good use case for that or was it an oversight that was discovered too late?
Would it make sense to let the library author decide how to transform A if this whole feature lands. Basically some kind of an attribute which would either transform it into B or in C.
My point is that feels like a half step backwards if a certain rethrowing function is known to be of the B kind but would be defaulted to C. There is literally nothing gained from that from the users perspective.
What different functions? Could you provide a small snippet so I may be able to understand it better. If you mean, multiple functions, then yeah it makes sense as Error is likely the common type between two different distinct error types.
My point is that I don't agree if a function like map would default to C instead of B.
public enum MyLibraryError: Error {
case errorProcessingHashedData(baseError: Error)
}
public func withHashedData<ReturnValue>(_ input: Data, _ body: (SHA256Digest) throws -> ReturnValue) rethrows -> ReturnValue {
let hashed = SHA256.hash(data: input)
do {
return try body(hashed)
} catch {
throw MyLibraryError.errorProcessingHashedData(baseError: error)
}
}
The idea being that of error encapsulation, which is not all that uncommon in other language ecosystems: we'll take the base error and add some metadata to it from our own context. The above example is a toy and has no reason to do error encapsulation, but that doesn't mean the idea is useless.
Wait, but this function doesn't rethrow anything. It throws it's own error type which is captured through do / catch block. The rethroning part would need the try body appear in the outer scope. So in theory this toy function has the wrong signature.
So it seems that option B is not there way to proceed even as one said, it is wieird not to control the type of the error bring rethrown, butt maybe this proposal is not the place for that
rethrows doesn't mean "only throws the errors the passed-in function throws", it means "only throws if the passed-in function throws". The above code compiles just fine, and the signature is correct.
The purpose of rethrows is to ensure that if you pass a closure to a rethrows function and the closure does not throw, you are not forced to write try around the outer function. It signals the circumstances in which a throw may happen.
I understand that, but it still bends my mind as I kinda focused on the idea of () -> Void meaning () throws Never -> Void. Would that even require rethrows to still need to exist?
I guess I need to re-read the previous thread to find the details why rethrows can't be typed.
@lukasa am I correct at least on the part that functions which are known to be of kind B and currently use rethrows could be adjusted in a typed throws world where () -> Void means () throws Never -> Void to use throw E where E is the error from the closure.
// before
func map<T>(_ transform: (Item) throws -> T) rethrows -> [T]
// after
func map<T, erased E: Error>(_ transform: (Item) throws E -> T) throws E -> [T]
If you pass a non-throwing function (which is then known to throw Never) the compiler won't require try from you. It preserves the same effect as rethrows.