> On Feb 20, 2017, at 3:46 PM, Karl Wagner <[email protected]> wrote:
>> On 20 Feb 2017, at 18:57, John McCall <[email protected] 
>> <mailto:[email protected]>> wrote:
>> 
>>> On Feb 19, 2017, at 3:04 PM, Anton Zhilin via swift-evolution 
>>> <[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.
>>> 
>> I agree.  And in general, this sort of thing is exactly my core concern 
>> about adding typed throws to the language: I am completely certain that many 
>> programmers will add typed throws annotations because they're programmers 
>> and thus, well, probably a little obsessive/compulsive, and they're trying 
>> to precisely document the behavior of their function without necessarily 
>> thinking about the usefulness of that information for their clients and (if 
>> they're writing a library; and really you should almost always be writing 
>> code as if you're writing a library) whether they're actually willing to 
>> commit to that behavior in their interface.  For those programmers, typed 
>> throws is just going to box them in and force them into anti-patterns in the 
>> long run.
>> 
>> In the vast majority of use-cases, clients are not going to exhaustively 
>> handle all errors — they will always have some generic fall-back.  That is 
>> not pessimism, it's actually the natural result of the complicated world we 
>> live in, where code can fail for a huge host of reasons and most callers 
>> won't have meaningful special-case behavior for all of them.  (On most 
>> operating systems, opening a file or a network connection can fail because 
>> you ran out of file descriptors.  You're seriously telling me that you're 
>> going to add a special case to your error logic for that?)  Go look at the 
>> actual error types that people use in most typed-throws situations and try 
>> to tell me I'm wrong — they probably have like twenty alternatives, and 
>> solidly a quarter or more of them will just be embedding some other 
>> arbitrarily-complex or stringly-typed error value.
>> 
>> The real use case for typed throws is when you have something like a parser 
>> library that really does only fail in a fixed number of semantically 
>> distinct ways, and you both (1) actually care about enforcing that in the 
>> implementation and making sure that other errors are handled internally and 
>> (2) you really do expect clients to exhaustively switch over the error at 
>> some point.  That's important.  But I continue to think that if adding 
>> better support for that use case misleads other programmers into thinking 
>> they should use typed throws, we will have made the language worse overall.
>> 
>> John.
>> 
>> 
>> 
>>> 2017-02-18 18:27 GMT+03:00 Karl Wagner <[email protected] 
>>> <mailto:[email protected]>>:
>>> 
>>> 
>>> 
>>> 
>>> So, I’m not sure about what was decided last time, but my issues with this 
>>> are:
>>> 
>>> - The thrown error type will become part of the ABI of the function. If you 
>>> change the type of Error that is thrown, callers may not catch it. At the 
>>> same time, if we make enums resilient by default and only allow specifying 
>>> a single entire type, you will basically need one Error enum per function 
>>> and it will need to be @fixed if you actually want to remove the catch-all 
>>> block. Otherwise:
>>> 
>>> // Let’s say this isn’t @fixed...
>>> enum CanFailError {
>>>     errorOne
>>>     errorTwo
>>> }
>>> 
>>> func canFail() throws(CanFailError) { /* … */ }
>>> 
>>> do { try canFail() }
>>> catch CanFailError {
>>>     switch error {
>>>         case .errorOne: /* handle error one */
>>>         case .errorTwo: /* handle error two */
>>>         default:        /* handle possible new errors in later versions of 
>>> the library */
>>>     }
>>> }
>>> 
>>> do { try canFail() }
>>> catch .errorOne { /* handle error one */ }
>>> catch .errorTwo { /* handle error two */  }
>>> catch           { /* handle possible new errors in later versions of the 
>>> library */ }
>>> 
>>> - I usually have _semantic_ namespaces for Errors, rather than single types 
>>> per implementation pattern. If we are adding strong annotations about which 
>>> errors can be thrown, I’d quite like to incorporate that pattern. For 
>>> example:
>>> 
>>> extension File {
>>> @fixed enum OpeningError { 
>>>     case .invalidPath
>>>     case .accessDenied  // e.g. asking for write permissions for read-only 
>>> file
>>> }
>>> @fixed enum ReadError {
>>>     case .invalidOffset // past EOF
>>>     case .deviceError   // probably worth aborting the entire operation the 
>>> read is part of
>>> }
>>> 
>>> // - throws:
>>> //     - .OpeningError if the file can’t be opened
>>> //     - .ReadError if the read operation fails
>>> func read(from offset: Int, into buffer: UnsafeBufferPointer<UInt8>) 
>>> throws(OpeningError, ReadError) { /* … */ }
>>> }
>>> 
>>> - I wonder if we could try something more ambitious. Since the list of 
>>> thrown errors is resilience-breaking for the function, it is only 
>>> beneficial for versioned and @inlineable functions. They should not be able 
>>> to add new errors (they can remove them though, since errors are intended 
>>> to be switched over). I wonder if we couldn’t introduce a small pattern 
>>> grammar for our structured comments (isolated from the rest of the 
>>> language) - it would be optional, but if you do list your errors, the 
>>> compiler would validate that you do it exhaustively. Some patterns I would 
>>> like are:
>>> 
>>> // - throws: - MyError.{errorOne, errorThree, errorFive}: Something bad     
>>>  || considered exhaustive
>>> @inlineable public func canFail() throws {}
>>> 
>>> // - throws: - OpeningError: Computer says nooooo...     || considered 
>>> exhaustive if OpeningError is versioned or @fixed
>>> //           - *                                         || other errors, 
>>> requires “catch-all” by external callers
>>> @inlineable public func canFail2() throws {}
>>> 
>>> If we want to get really clever, we can have the compiler automatically 
>>> generate those error-lists for internal functions, so you would 
>>> automatically get exhaustive error-handling within your own module.
>>> 
>>> - Karl
>>> 
>>> 
>>> _______________________________________________
>>> swift-evolution mailing list
>>> [email protected] <mailto:[email protected]>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
> 
> I agree, and that’s where I was going with it: I think that typed-throws 
> should basically be something on the level of a stronger comment rather than 
> something so definitive as the function’s ABI. That’s how it will be much of 
> the time in practice, anyway.
> 
> I don’t believe having a single error type is really ideal for anything. 
> We’ve basically whittled down the feature until it gets in the way. If every 
> function is throwing its own enum or hidden under complex hierarchies of 
> protocols, it becomes difficult to write helper routines which respond to 
> common errors in certain ways (e.g. trying an operation if if failed because 
> the network was down).
> 
> 
> // Using one-enum per function
> 
> enum FunctionOneError: Error {
>     case networkWasDown(shouldTryAgain: Bool)
>     case otherReason
> }
> func functionOne() throws(FunctionOneError)
> 
> enum FunctionTwoError: Error {
>     case networkWasDown(shouldTryAgain: Bool)
>     case aDifferentReason
> }
> func functionTwo() throws(FunctionTwoError)
> 
> // How to use this information at a high level?
> 
> func retryIfNetworkDown(let attempts: Int = 3, work: ()throws->Void) rethrows 
> -> Bool { // <- Can’t specify which errors we take, or which we rethrow
>     for n in 0..<attempts {
>         do { try work() }
>         catch FunctionOneError.networkWasDown(let tryAgain) {
>             if tryAgain, n<attempts { continue }
>             else { return false }
>         }
>         catch FunctionTwoError.networkWasDown(let tryAgain) {  // Needs to 
> handle per-function errors :(
>             if tryAgain, n<attempts { continue }
>             else { return false }
>         }
>         catch { throw error }
>     }
> }
> 
> So I’ve heard people say you should create a protocol then, but that’s not 
> really a convenient solution either...
> 
> protocol NetworkError {
>     func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool)
> }
> 
> extension FunctionOneError: NetworkError {
>     func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool) {
>         guard case .networkWasDown(let tryAgain) = self else { return (false, 
> false) }
>         return (true, tryAgain)
>     }
> }
> 
> // This needs to be done twice, too...
> 
> extension FunctionTwoError: NetworkError {
>     func wasNetworkDownAndShouldTryAgain() -> (Bool, Bool) {
>         guard case .networkWasDown(let tryAgain) = self else { return (false, 
> false) }
>         return (true, tryAgain)
>     }
> }
> 
> 
> So I think it all descends in to lots of syntax for very marginal amounts of 
> value. Typed-throws is never likely to be the wondrous 100% cross-library 
> reliability guarantee that people dream of. I view it more like good 
> documentation.

I completely agree that what most programmers are looking for is just a blessed 
way to document that a function is likely to throw specific kinds of error, and 
that certain of them might be worth considering in the caller.

John.

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to