> 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

Reply via email to