[swift-evolution] [Pitch] Typed throws
Karl Wagner
razielim at gmail.com
Tue Feb 21 02:01:21 CST 2017
> On 21 Feb 2017, at 00:34, Karl Wagner <karl.swift at springsup.com> wrote:
>
>
>> On 19 Feb 2017, at 21:04, Anton Zhilin <antonyzhilin at gmail.com <mailto:antonyzhilin at gmail.com>> wrote:
>>
>> It’s expected that if you need resilience, then you will throw an “open” enum. Essentially, we pass resilience of typed throws on to those who will hopefully establish resilience of enums.
>>
>> If you prefer separate error types, then declare a base protocol for all your error types and throw a protocol existential. You won’t even need default case in switches, if closed protocols make it into the language.
>>
>> I don’t like any solution that is based on comments. I think that compiler should always ignore comments.
>>
>>
>
> Open enums can only add cases, not remove them. That means that new versions of a function will similarly only be able to add errors, and won’t be able to communicate that certain errors are no longer thrown. Protocols aren’t a solution, because you will need to write verbose conformances for all of your Error types.
>
> Let me put this in another (perhaps more palatable) way. Forget comments, let’s say its part of some hidden metadata:
>
> - The compiler lists every error which can be thrown by a function (it’s easily able to do that)
> - Rather than making this part of the signature, the signature only says “throws an Error” and the actual Error list is written somewhere in the module as documentation/metadata.
>
> Here’s why it’s so good:
>
> - It’s completely free (you don’t write anything). The compiler generates everything for you.
> - It’s **completely optional**: it won’t make your structure your Errors in a way that's less usable in a type-system sense for clients who don’t care about exhaustive catching.
> - Exhaustive catching within your own module for free (you can omit a catch-all, the compiler won’t complain)
>
> It’s good for resiliency, too:
>
> - Non-resilient functions always reserve the right to throw new Errors in later versions. No system will get exhaustive catching for them anyway.
> - If you stop throwing a specific Error, nothing breaks - it simply vanishes from the documentation/metadata. The compiler can simply warn about the redundant catch.
> - Resilient (@versioned) functions can still offer exhaustive catching if we want to offer that. We might decide to make that opt-in/opt-out, because it would mean they would be limited to removing Errors, and never adding new ones.
> —> As with any ABI promise, we will trap or it will be UB if you break the contract. We couldn’t validate it when compiling, but theoretically a validator could be built which compared two library versions.
>
> - Karl
>
So here’s my counter-proposal, fleshed out:
Specially, on resiliency:
## Internally to a module
Compiler can use generated Error-list metadata to:
- provide compile-errors with specific uncaught errors (better diagnostics)
- allow omitting catch-alls
- optimise away Error existential allocations
All of that would automatically apply to all throwing/rethrowing functions, without any additional developer effort.
## Cross-module
Compiler can use generated Error-list metadata to:
- Inform users about errors that might get thrown by this version of the function (purely documentation)
- Allow omitting catch-alls for specific functions which opt-in to that contract.
And that’s it. Notice there is no behavioural change; the Error-list metadata is entirely optional.
### Exhaustive catching cross-module
Resilient functions can _additionally_ promise to never throw new Errors. It should be an additional promise. From an error-list perspective, the function makes an additional promise that later versions of the error-list will never get more inclusive.
We can’t check that resilience at compile-time, though. If you change the function signature, you will get a error in the dynamic linker. Similarly, if somebody just adds a case to their @fixed enum, you won’t know until runtime when it gets thrown and nobody’s there to catch it.
- It would be cool if we failed gracefully in that case; if the caller wasn’t catching exhaustively, the new error should fall in to the existing catch-all.
- Otherwise, if the caller was assuming the library author kept their promise and omitted a catch-all, we should still synthesise one to provide a unique trap location (something like swift_resilience_unexpectedError()).
- It means we can't optimise away the Error existential cross-module (maybe in -Ounchecked?), but that seems to me like an acceptable cost.
The big problem with this is that it relies on unwritten, generated metadata. For resilient functions promising resilient error-lists, it’s helpful to have the errors you’re promising written down and easily manually inspectable. That’s why I initially suggested having the compiler validate the documentation comments. We could still do that - so if you have a @versioned function and you additionally say that the errors it throws are also @versioned, you have to write a comment listing every Error (and the compiler will check it).
Small example:
enum FailureReason {
case deviceBusy
case networkDown
case notFound
}
//% [Error-list]: FailureReason.notFound
func openFile(_ path: String) throws { … }
//% @versioned [Error-list]: FailureReason.notFound, FailureReason.deviceBusy
@versioned(includingErrors)
func read(_ path: String, range: Range<Int>) -> Data { … }
//% [Error-list]: <rethrows from arg1>
func retrying(attempts n: Int, _ work: ()throws -> Void) rethrows -> Bool {
for _ in 0..<attempts {
do { try work(); return true }
catch FailureReason.deviceBusy { continue }
catch FailureReason.networkDown { continue }
catch { throw error }
}
return false
}
//% [Error-list]: <rethrows from arg0, masks: FailureReason.notFound>
func mustBeFound(_ work: ()throws -> Void) rethrows {
do { try work() }
catch FailureReason.notFound { fatalError(“This thing must be found") }
catch { throw error }
}
mustBeFound { openFile(“test.txt”) } // can be proven not to throw (in same module), because mustBeFound handles all errors
// cross-module
// Function does not have a versioned error-list; catch-all is mandatory.
do { try openFile(“test.txt”) }
catch .notFound { print(“not found!”) }
catch { print(“other error: \(error}”) }
do { try mustBeFound { open(“test.txt”) } }
catch { print(“other error: \(error}”) }
// Function has a versioned error-list; catch-all is optional.
do { try readFile(“test.txt”, range: 0..<64) }
catch .notFound { print(“file not found!”) }
catch .deviceBusy { /* maybe retry? */ }
// [implicit] catch { _swift_reslience_unexpectedError() }
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170221/66b2ae51/attachment.html>
More information about the swift-evolution
mailing list