> 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

Reply via email to