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

Reply via email to