Right, but since this would affect the Obj-C importer and thus would be a source-breaking change, it would probably not be possible anymore after Swift 3.
Charles > On Jul 14, 2016, at 4:57 PM, Dan Stenmark <[email protected]> wrote: > > I’d say it’s a little premature to be talking about this; the team has made > it very clear that the discussion on Native Concurrency in Swift won’t begin > for another couple months. > > Dan > >> On Jul 14, 2016, at 10:54 AM, Charles Srstka via swift-evolution >> <[email protected]> wrote: >> >> I know it’s late, but I was wondering what the community thought of this: >> >> MOTIVATION: >> >> With the acceptance of SE-0112, the error handling picture looks much >> stronger for Swift 3, but there is still one area of awkwardness remaining, >> in the area of returns from asynchronous methods. Specifically, many >> asynchronous APIs in the Cocoa framework are declared like this: >> >> - (void)doSomethingWithFoo: (Foo *)foo completionHandler: (void (^)(Bar * >> _Nullable, NSError * _Nullable))completionHandler; >> >> This will get imported into Swift as something like this: >> >> func doSomething(foo: Foo, completionHandler: (Bar?, Error?) -> ()) >> >> The intention of this API is that either the operation will succeed, and >> something will be passed in the Bar parameter, and the error will be nil, or >> else the operation will fail, and then the error parameter will be populated >> while the Bar parameter is nil. However, this intention is not expressed in >> the API, since the syntax leaves the possibility that both parameters could >> be nil, or that they could both be non-nil. This forces the developer to do >> needless and repetitive checks against a case which in practice shouldn’t >> occur, as below: >> >> doSomething(foo: foo) { bar, error in >> if let bar = bar { >> // handle success case >> } else if let error = error { >> self.handleError(error) >> } else { >> self.handleError(NSCocoaError.FileReadUnknownError) >> } >> } >> >> This results in the dreaded “untested code.” >> >> Note that while it is possible that the developer could simply force-unwrap >> error in the failure case, this leaves the programs open to crashes in the >> case where a misbehaved API forgets to populate the error on failure, >> whereas some kind of default error would be more appropriate. The >> do/try/catch mechanism works around this by returning a generic _NilError in >> cases where this occurs. >> >> PROPOSED SOLUTION: >> >> Since the pattern for an async API that returns an error in the Cocoa APIs >> is very similar to the pattern for a synchronous one, we can handle it in a >> very similar way. To do this, we introduce a new Result enum type. We then >> bridge asynchronous Cocoa APIs to return this Result type instead of >> optional values. This more clearly expresses to the user the intent of the >> API. >> >> In addition to clarifying many Cocoa interfaces, this will provide a >> standard format for asynchronous APIs that return errors, opening the way >> for these APIs to be seamlessly integrated into future asynchronous features >> added to Swift 4 and beyond, in a way that could seamlessly interact with >> the do/try/catch feature as well. >> >> DETAILED DESIGN: >> >> 1. We introduce a Result type, which looks like this: >> >> enum Result<T> { >> case success(T) >> case error(Error) >> } >> >> 2. Methods that return one parameter asynchronously with an error are >> bridged like this: >> >> func doSomething(foo: Foo, completionHandler: (Result<Bar>) -> ()) >> >> and are used like this: >> >> doSomething(foo: foo) { result in >> switch result { >> case let .success(bar): >> // handle success >> case let .error(error): >> self.handleError(error) >> } >> } >> >> 3. Methods that return multiple parameters asynchronously with an error are >> bridged using a tuple: >> >> func doSomething(foo: Foo, completionHandler: (Result<(Bar, Baz)>) -> ()) >> >> and are used like this: >> >> doSomething(foo: foo) { result in >> switch result { >> case let .success(bar, baz): >> // handle success >> case let .error(error): >> self.handleError(error) >> } >> } >> >> 4. Methods that return only an error and nothing else are bridged as they >> are currently, with the exception of bridging NSError to Error as in SE-0112: >> >> func doSomething(foo: Foo, completionHandler: (Error?) -> ()) >> >> and are used as they currently are: >> >> doSomething(foo: foo) { error in >> if let error = error { >> // handle error >> } else { >> // handle success >> } >> } >> >> 5. For the case in part 2, the bridge works much like the do/try/catch >> mechanism. If the first parameter is non-nil, it is returned inside the >> .success case. If it is nil, then the error is returned inside the .error >> case if it is non-nil, and otherwise _NilError is returned in the .error >> case. >> >> 6. For the case in part 3, in which there are multiple return values, the >> same pattern is followed, with the exception that we introduce a new >> Objective-C annotation. I am provisionally naming this annotation >> NS_REQUIRED_RETURN_VALUE, but the developer team can of course rename this >> annotation to whatever they find appropriate. All parameters annotated with >> NS_REQUIRED RETURN_VALUE will be required to be non-nil in order to avoid >> triggering the error case. Parameters not annotated with NS_REQUIRED >> RETURN_VALUE will be inserted into the tuple as optionals. If there are no >> parameters annotated with NS_REQUIRED RETURN_VALUE, the first parameter will >> be implicitly annotated as such. This allows asynchronous APIs to continue >> to return optional secondary values if needed. >> >> Thus, the following API: >> >> - (void)doSomethingWithFoo: (Foo *)foo completionHandler: (void (^)(Bar * >> _Nullable NS_REQUIRED_RETURN_VALUE, Baz * _Nullable >> NS_REQUIRED_RETURN_VALUE, NSError * _Nullable))completionHandler; >> >> is bridged as: >> >> func doSomething(foo: Foo, completionHandler: (Result<(Bar, Baz)>) -> ()) >> >> returning .success only if both the Bar and Baz parameters are non-nil, >> whereas this API: >> >> - (void)doSomethingWithFoo: (Foo *)foo completionHandler: (void (^)(Bar * >> _Nullable NS_REQUIRED_RETURN_VALUE, Baz * _Nullable, NSError * >> _Nullable))completionHandler; >> >> is bridged as: >> >> func doSomething(foo: Foo, completionHandler: (Result<(Bar, Baz?)>) -> ()) >> >> returning .success whenever the Bar parameter is nil. An API containing no >> parameter annotated with NS_REQUIRED_RETURN_VALUE will be bridged the same >> as above. >> >> FUTURE DIRECTIONS: >> >> In the future, an asynchronous API returning a Result could be bridged to an >> async function, should those be added in the future, using the semantics of >> the do/try/catch mechanism. The bridging would be additive, similarly to how >> Objective-C properties declared via manually written accessor methods can >> nonetheless be accessed via the dot syntax. Thus, >> >> func doSomething(_ completionHandler: (Result<Foo>) -> ()) >> >> could be used as if it were declared like this: >> >> async func doSomething() throws -> Foo >> >> and could be used like so: >> >> async func doSomethingBigger() { >> do { >> let foo = try await doSomething() >> >> // do something with foo >> } catch { >> // handle the error >> } >> } >> >> making asynchronous APIs convenient to write indeed. >> >> ALTERNATIVES CONSIDERED: >> >> Leaving the somewhat ambiguous situation as is. >> >> Charles >> >> _______________________________________________ >> swift-evolution mailing list >> [email protected] >> https://lists.swift.org/mailman/listinfo/swift-evolution > _______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
