> On Dec 18, 2015, at 9:25 AM, Dennis Lysenko <[email protected]> > wrote: > > Genuinely, David, thank you for taking up the mantle of this problem. To me, > the lack of type annotations makes the error handling painful and I wish the > team just hadn't released it until the design was smoothed out fully. Those > dangling catch-all blocks when I've caught all cases make it uselessly > verbose and moreover do not fit with the language at all. > > As for the multiple vs. single type annotations, I do think that you need a > concrete example of why this will be different from java to quell the > concerns of all the people that skim the proposal, don't think about it, and > exclaim "but everyone hates it in Java!" so I agree on the singular type > annotations. > I definitely agree that we need to address the Java problem. That said, I don’t think avoiding knee-jerk objection from people who don’t read and consider a proposal carefully is a good way to approach design. There may be good reasons to choose singular type annotations but this is not one of them.
I believe Félix raised good points about polymorphism and complex hierarchy causing problems in Java. That sounds like it is the source of at least a significant part of the problems with Java’s checked exception model. > Also, your point about being able to mark functions async strongly supports > single type annotations until we get union types (if ever). > > I am glad you addressed covariance/contravariance and the semantics of > function types when they throw general errors vs. a specific one. > > One question, mainly to the compiler team: would it be reasonable to be able > to use generics covariant over the throws operator? For example, I could > define a function that takes a function which throws E and returns R, and > creates a function that takes a callback which takes an argument of type > Either<R, E> instead. This would be an incredibly powerful feature. > > > On Fri, Dec 18, 2015, 8:53 AM Matthew Johnson via swift-evolution > <[email protected] <mailto:[email protected]>> wrote: > David, > > Thank you for taking the time to continue working on a proposal for typed > throws. I agree that this feature is very desirable and appreciate the work > you’re doing to bring forward a proposal. I think it’s a great start but > also has some room for improvement. > > First, I think it could be strengthened by incorporating some learning from > Rust. My impression is that the Rust community is very happy with typed > error handling. Adding some detail about their experience would provide a > counter-example to those who are concerned about the experience in Java and > C++. > > I agree that error types are an important part of an API contract. One of > the big hurdles to doing this well is the need to catch errors when all that > needs to be done is to wrap and rethrow them. Ideally should not need to do > this just to perform a simple type translation to map the underlying error > into the type we wish to expose as part of a stable API contract. You might > want to take a look at the From mechanism Rust uses to facilitate this. IMO > a proposal for typed error handling should address this issue in some way > (even if the author determines this mechanism is not necessary or a good > design cannot be identified). > > I would also like to see much more detail on why you think allowing a > function to throw multiple error types is problematic. My impression is that > you have concerns from a usability point of view. I am on the fence here to > some degree, but definitely leaning in the direction that allowing a function > to throw multiple error types is better. > > The primary reason I lean this way is that it enables more re-use of standard > error types. Custom error types for an API often make sense, but not always. > I am concerned about the need to create them just because our API contract > might reasonably include two or three of the standard error types. Adding > new types when they are not necessary introduces complexity and cognitive > overhead. It also complicates catching of errors if the new custom type is a > two or three case enum that just embeds the underlying error. > > These problems will lead many people to just revert to an untyped throws > clause. Objections to typed errors along these lines are common and > legitimate. They will arise during review. It is best if you address them > in the proposal now in order to focus a review on your solutions. My > personal opinion is that allowing multiple error types and including a > mechanism to perform automatic wrapping when appropriate would go a long way > towards solving them. > > Implementation challenges related to multi-typed errors have been discussed > on the list quite a bit already. They would obviously need to be addressed > if we go in that direction. I don’t want to downplay those. But I do think > we need to try to identify the most usable solution for typed errors that we > can first and then focus on implementation details. If the design needs to > be modified to accommodate implementation at least we will have a better idea > of what we are giving up. > > I am willing to be convinced that a single error type is better than multiple > error types but the current proposal does not provide a compelling argument > in that direction. It just says “Java checked exceptions”. I know these > have been pretty much universally considered a serious design mistake. My > impression is that there are quite a few reasons for that. I don’t have any > direct experience with Java and am not familiar with the details. If you > could elaborate on specifically why you believe allowing multiple error types > was a significant contributor to the problem in a manner that indicates that > they will be a problem in any language that includes them I would appreciate > that. Links would be sufficient if they are focused on answering this > particular question. > > I’m looking forward to your feedback on these thoughts. > > Thanks, > Matthew > > >> On Dec 18, 2015, at 1:29 AM, David Owens II via swift-evolution >> <[email protected] <mailto:[email protected]>> wrote: >> >> This a significantly updated proposal for typed annotations on the `throws` >> construct. The previous was closed due to not be complete; I believe I’ve >> addressed all of those concerns. >> >> https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md >> >> <https://github.com/owensd/swift-evolution/blob/master/proposals/allow-type-annotations-on-throw.md> >> >> — >> >> Allow Type Annotation on Throws >> Proposal: SE-NNNN <> >> Author(s): David Owens II <> >> Status: Pending Approval for Review >> Review manager: TBD >> Introduction >> The error handling system within Swift today creates an implicitly loose >> contract on the API. While this can be desirable in some cases, it’s >> certainly not desired in all cases. This proposal looks at modifying how the >> error handling mechanism works today by adding the ability to provide a >> strong API contract. >> >> Error Handling State of the Union >> This document will use the terminology and the premises defined in the Error >> Handling Rationale >> <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> >> document. >> >> To very briefly summarize, there are four basic classification of errors: >> >> Simple Domain Errors >> Recoverable Errors >> Universal Errors >> Logic Failures >> Each of these types of errors are handled differently at the call sites. >> Today, only the first two are directly handled by Swift error handling >> mechanism. The second two are uncatchable in Swift (such as fatalError(), >> ObjC exceptions, and force-unwrapping of null optionals). >> >> Simple Domain Errors >> >> As stated in Error Handling Rationale >> <https://github.com/apple/swift/blob/master/docs/ErrorHandlingRationale.rst> >> document, the “Swift way” to handle such errors is to return an Optional<T>. >> >> func parseInt(value: String) -> Int? {} >> The simple fact of the result being Optional.None signifies that the string >> could not be parsed and converted into an Int. No other information is >> necessary or warranted. >> >> Recoverable Errors >> >> In this context, these are errors that need to provide additional >> information to the caller. The caller can then decide a course of action >> that needs to be taken. This could be any number of things, including, but >> not limited to, logging error information, attempting a retry, or >> potentially invoking a different code path. All of these errors implement >> the ErrorType protocol. >> >> func openFile(filename: String) throws {} >> The throws keyword annotates that the function can return additional error >> information. The caller must also explicitly make use of this when invoking >> the function. >> >> do { >> try openFile("path/to/somewhere") >> } >> catch {} >> Errors are able to propagate if called within another context that can >> throw, thus alleviating the annoying “catch and rethrow” behavior: >> >> func parent() throws { >> try openFile("path/to/somwhere") >> } >> Lastly, functions can be marked to selectively throw errors if they take a >> function parameter that throws with the rethrows keyword. The really >> interesting part is that it’s only necessary to use try when calling the >> function with a throwing closure. >> >> func openFile(filename: String) throws {} >> func say(message: String) {} >> >> func sample(fn: (_: String) throws -> ()) rethrows { >> try fn("hi") >> } >> >> try sample(openFile) >> sample(say) >> Converting Recoverable Errors to Domain Errors >> >> Swift also has the try? construct. The notable thing about this construct is >> that it allows the caller to turn a “Recoverable Error” into a “Simple >> Domain Error”. >> >> if let result = try? openFile("") {} >> ErrorType Implementors >> >> Errors are implemented using the ErrorType protocol. Since it is a protocol, >> new error types can be a class, a struct, or an enum. A type qualified >> throws clause would allow code authors to change the way that the >> catch-clauses need to be structured. >> >> Enum Based ErrorType >> >> When enums are used as the throwing mechanism, a generic catch-clause is >> still required as the compiler doesn’t have enough information. This leads >> to ambiguous code paths. >> >> enum Errors: ErrorType { >> case OffBy1 >> case MutatedValue >> } >> >> func f() throws { throw Errors.OffBy1 } >> >> do { >> try f() >> } >> catch Errors.OffBy1 { print("increment by 1") } >> catch Errors.MutatedValue { fatalError("data corrupted") } >> The above code requires a catch {} clause, but it’s ambiguous what that case >> should do. There is no right way to handle this error. If the error is >> ignored, we’re now in the land of “Logic Errors”; the code path should never >> be hit. If we use a fatalError() construct, then we are now in the land of >> converting a potential compiler error into a “Universal Error”. >> >> Both of these are undesirable. >> >> Struct and Class Based ErrorType >> >> In the current design, errors that are thrown require a catch-all all the >> time. In the proposed design, which will be explained further, a catch-all >> would not be required if there was a case-clause that matched the base type. >> >> class ErrorOne: ErrorType {} >> func g() throws { throw ErrorOne() } >> >> do { >> try g() >> } >> catch is ErrorOne { print("ErrorOne") } >> The advantage in these cases are different, these cases do not allow pattern >> matching over the error type members (as you can in a switch-statement, for >> example). >> >> The workaround for this functionality is this: >> >> class ErrorOne: ErrorType { >> let value: Int >> init(_ value: Int) { self.value = value } >> } >> >> do { >> try g() >> } >> catch { >> if let e = error as? ErrorOne { >> switch e { >> case _ where e.value == 0: print("0") >> case _ where e.value == 1: print("1") >> default: print("nothing") >> } >> } >> } >> This proposal would turn the above into: >> >> class ErrorOne: ErrorType { >> let value: Int >> init(_ value: Int) { self.value = value } >> } >> >> do { >> try g() >> } >> catch _ where error.value == 0 { print("0") } >> catch _ where error.value == 1 { print("1") } >> catch { print("nothing") } >> } >> No gymnastics to go through, just straight-forward pattern-matching like >> you’d expect. >> >> NOTE: This requires the promotion of the error constant to be allowed >> through the entirety of the catch-clauses. >> >> Overriding >> >> In the context of types, it’s completely possible to override functions with >> the throws annotations. The rules simply follow the rules today: covariance >> on the return type is allowed, contravariance is not. >> >> Generics >> >> When looking at generics, I cannot come up with a reason why they shouldn’t >> just work as normal: >> >> func gen<SomeError: ErrorType>() throws SomeError {} >> The only constraint would be that the specified error type must adhere to >> the ErrorType protocol. However, this is no different than today: >> >> func f<T>(a: T) throws { throw a } >> This results in the compiler error: >> >> Thrown expression type ’T’ does not conform to ‘ErrorType’ >> This seems like it should “just work”. >> >> Design Change Proposal >> The design change is simple and straight-forward: allow for the annotation >> of the type of error that is being returned as an optional restriction. The >> default value would still be ErrorType. >> >> func specific() throws MyError {} >> func nonspecific() throws {} >> There is a secondary result of this proposal: the error constant should be >> promoted to be allowed for use through-out all of the catch-clauses. >> >> Impact on Existing Code >> >> This is a non-breaking change. All existing constructs work today without >> change. That said, there are a few places where this change will have an >> impact on future usage. >> >> Function Declarations >> >> When a function has a throws clause that is attributed with a type, then >> that type becomes part of the function signature. This means that these two >> functions are not considered to be of the same type: >> >> func one() throws {} >> func two() throws NumberError {} >> The function signatures are covariant though, so either one or two can be >> assigned to f below: >> >> let f: () throws -> () >> This is completely fine as NumberError still implements the ErrorType >> protocol. >> >> However, in this case: >> >> let g: () throws NumberError -> () >> It would not be valid to assign one to g as the type signature is more >> specific. >> >> throws and rethrows >> >> Functions currently have the ability to be marked as rethrows. This >> basically says that if a closure parameter can throw, then the function will >> throw too. >> >> func whatever(fn: () throws -> ()) rethrows {} >> The whatever function is up for anything that fn is up for. Keeping in line >> with this mentality, the rethrows would exhibit the same behavior: typed >> annotations simply apply if present and do not if they are missing. >> >> func specific(fn: () throws HappyError -> ()) rethrows {} >> This all works as expected: >> >> func f() throws HappyError {} >> func g() {} >> >> try specific(f) >> specific(g) >> This works for the same covariant reason as the non-qualified throws >> implementation works: a non-throwing function is always able to be passed in >> for a throwing function. >> >> The do-catch statement >> >> There are two rule changes here, but again, it’s non-breaking. >> >> The first rule change is to promote the error constant that would normally >> only be allowed in the catch-all clause (no patterns) to be available >> throughout each of the catch clauses. This allows for the error information >> to be used in pattern matching, which is especially valuable in the non-enum >> case. >> >> The second change is to allow the error constant to take on a specific type >> when all of the throwing functions throw the same specified type. When this >> is the case, two things become possible: >> >> In the enum-type implementation of ErrorType, the catch-clauses can now be >> exhaustive. >> In the all of the cases, the API of the specific ErrorType becomes available >> in the catch-clause without casting the error constant. This greatly >> simplifies the pattern-matching process. >> In the case that there are heterogenous ErrorType implementations being >> returned, the errorconstant simply has the type of ErrorType. >> >> The try call sites >> >> There is no change for the try, try?, or try! uses. The only clarification >> I’ll add is that try?is still the appropriate way to promote an error from a >> “Recoverable Error” to a “Simple Domain Error”. >> >> Alternate Proposals >> There is another common error handling mechanism used in the community >> today: Either<L, R>. There are various implementations, but they all >> basically boil down to an enum that captures the value or the error >> information. >> >> I actually consider my proposal syntactic sugar over this concept. If and >> when Swift supports covariant generics, there is not a significant reason I >> can see why the underlying implementation could not just be that. >> >> The advantage is that the proposed (and existing) syntax of throws greatly >> increases the readability and understanding that this function actually >> possesses the ability to throw errors and they should be handled. >> >> The other advantage of this syntax is that it doesn’t require a new >> construct to force the usage of the return type. >> >> Further, if functions where to ever gain the ability to be marked as async, >> this could now be handled naturally within the compiler as the return type >> could a promise-like implementation for those. >> >> Criticisms >> From the earlier threads on the swift-evolution mailing list, there are a >> few primary points of contention about this proposal. >> >> Aren’t we just creating Java checked-exceptions, which we all know are >> terrible? >> >> No. The primary reason is that a function can only return a single >> error-type. The other major reason is that the error philosophy is very >> different in Swift than in Java. >> >> Aren’t we creating fragile APIs that can cause breaking changes? >> >> Potentially, yes. This depends on how the ABI is handled in Swift 3 for >> enums. The same problem exists today, although at a lesser extent, for any >> API that returns an enum today. >> >> Chris Lattner mentioned this on the thread: >> >> The resilience model addresses how the public API from a module can evolve >> without breaking clients (either at the source level or ABI level). >> Notably, we want the ability to be able to add enum cases to something by >> default, but also to allow API authors to opt into more >> performance/strictness by saying that a public enum is “fragile” or “closed >> for evolution”. >> So if enums have an attribute that allows API authors to denote the >> fragility enums, then this can be handled via that route. >> >> Another potential fix is that only internal and private scoped functions are >> allowed to use the exhaustive-style catch-clauses. For all public APIs, they >> would still need the catch-all clauses. >> >> For APIs that return non-enum based ErrorType implementations, then no, this >> does not contribute to the fragility problem. >> >> Aren’t we creating the need for wrapper errors? >> >> This is a philosophical debate. I’ll simply state that I believe that simply >> re-throwing an error, say some type of IO error, from your API that is not >> an IO-based API is design flaw: you are exposing implementation details to >> users. This creates a fragile API surface. >> >> Also, since the type annotation is opt-in, I feel like this is a really >> minor argument. If your function is really able to throw errors from various >> different API calls, then just stick with the default ErrorType. >> >> _______________________________________________ >> 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> > > _______________________________________________ > 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>
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
