> On 21 Feb 2017, at 00:34, Karl Wagner <[email protected]> wrote:
>
>
>> On 19 Feb 2017, at 21:04, Anton Zhilin <[email protected]
>> <mailto:[email protected]>> 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() }
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution