Has anyone else wanted a negated type pattern in catch? I couldn't find an earlier thread on this.
Particularly since the introduction of typed-throws (SE-0413) I find myself often writing a forwarding clause when a do body mixes different typed and untyped throws (or just different typed throws).
func fn() throws(MyError) {
do {
try someTypedThrows() // throws(MyError)
try someUntypedThrows()
} catch let error as MyError {
throw error // making sure it doesn't end up wrapped in .unhandled
} catch {
throw .unhandled(error)
}
}
I often feel this would be preferable:
func fn() throws(MyError) {
do {
try someTypedThrows()
try someUntypedThrows()
} catch not MyError {
throw .unhandled(error)
} // only a `MyError` can escape
}
The clause would be non-exhaustive, so anything it doesn't match propagates, and the compiler could narrow the escaping type to MyError.
A negated-catch could co-exist with other patterned-catch, but I think it would likely have to be mutually exclusive with a plain-catch as it would only ever see MyError, so it’s redundant. I also foresee that the negated-catch would work in a function that does not declare a typed-throws:
func fn() throws {
do {
try someUntypedThrows()
} catch not MyError {
throw MyError.unhandled(error)
} // only a `MyError` can escape
}
I wonder then, in the above example, if the compiler would/could infer the throws as throws(MyError) instead of throws(any Error)?
I'm not set on the spelling. Mirroring the existing catch is MyError and catch let error as MyError would yield the spellings catch not MyError and catch let error not MyError; is not would read better, but I'm unsure whether requiring two keywords is desirable.
Having a not keyword would very likely not be accepted as it is explicitly barred for Swift Evolution proposals for Boolean values like is provides. If you really want these semantics, you might want to seek ~is/!is as a keyword. The keyword is only exists in its current contexts as it was distinctly different from the functionality conveyed by :[1] and is usable in a less restrictive environment.
It is the best at conveying the information which does not already have a term or symbol of art and pairs with as as metatype operators.
Also, as you already stated, you can already express the functionality as you previously mentioned.
A do catch block is effectively a switch statement as demonstrated below
switch Result(catching: {
try someTypedThrows()
try someUntypedThrows()
}) {
case .success: NOP()
case .failure(let error as MyError): throw error
case .failure(let error): throw MyError.unhandled(error)
}
A switch statement must be exhaustive and must explicitly acknowledge all paths. By saying, catch unless it is type MyError you imply a new catch clause that rethrows. This would impair reasoning in you code as you have to figure out how a particular error is handled and would imply that you can disregard a specific case in a switch statement which is simply not true.
I'm not sure if it would be good but it seems like if your error enum can be constructed for arbitrary Error types, and the purpose of this syntax is just to rethrow or wrap, the compiler could do that for you and you can just write try.
protocol ConvertableFromError: Error {
/// Compiler promises to only call this init after attempting to as? cast your error.
init(wrapping: some Error)
}
And the implementation could map to cases for specific known errors...
I'm not sure how much this buys over just using untyped errors. I guess you get exhaustiveness over the cases in MyError, which is good, and the ergonomics of automatically wrapping arbitrary errors. And the compiler can do the as? Self check for you, potentially more efficiently?
I bet this idea has been floated before.
Also seems like it would bring us closer to implementing anyhow / eyre esc library in Swift ergonomically, although I think that'd need answers for how to handle passing through untyped throws in generic code.
True, there is this and I will admit I do often forget to reach for a where clause on catch specifically.
It is more verbose and I'd say takes more effort to parse at a glance, so I do feel a special case spelling could still add value.
My understanding is it is explicitly barred for replacements of operators, which this proposal isn't suggesting.
I had considered !is/!is but felt that Swift didn't have a precedent for a symbol + word as a keyword and may get push back from the complexity it would add to the lexer.
I think I do come back to the same thing I mentioned first up with FlorianPircher, I agree that this works, it's perhaps even more verbose and compared to the where-clause I'd say carries even more cognitive complexity; it's much harder to ascertain at a glance what it's doing, do-catch blocks are better at this.
I will admit it was perhaps too contrived of an example, while there are definitely scenarios I have where I'm just wrapping/rethrowing, there's also plenty more where the non-forwarding catch blocks contain business logic before rethrowing (and wrapping if I want to use typed-throws)