> 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
