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

Reply via email to