[PITCH] Improved error handling for async Cocoa methods


(Charles Srstka) #1

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


(Dan Stenmark) #2

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 <swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dan Appel) #3

Result is one way to do it, but I'm not a huge fan. I would personally
prefer this kind of api.

doSomethingAsync { getResult in
do {
let result = try getResult()
} catch {
// handle error
}
}

Where the signature would go from

func doSomethingAsync(callback: (T?, Error?) -> ())

to

func doSomethingAsync(callback: (() throws -> T) -> ())

No new types defined; the current callbacks just have to be wrapped in
another closure.

···

On Thu, Jul 14, 2016 at 10:54 AM Charles Srstka via swift-evolution < swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel


(Charles Srstka) #4

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 <daniel.j.stenmark@gmail.com> 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 <swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dan Stenmark) #5

IIRC, one of the main goals in Swift 3 was to further stabilize the language spec and *minimize* breakage in future versions, not eliminate potential breakage altogether. Considering the numerous directions Native Concurrency can take (Go-style green thread scheduling, async-await, continuation handler transforms, etc), it would be much better if that discussion got started first. Asynchronous error handling will likely be a big talking point of said discussion anyway.

Dan

···

On Jul 14, 2016, at 3:30 PM, Charles Srstka <cocoadev@charlessoft.com> wrote:

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 <daniel.j.stenmark@gmail.com> 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 <swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #6

When/if we get an async/await like feature, of course we’ll try to pull completion handlers automatically into the model. We can do that post swift 3.

-Chris

···

On Jul 14, 2016, at 3:30 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

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 <daniel.j.stenmark@gmail.com> 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 <swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Dan Appel) #7

Yes, it should be made clear that the 'async'/'await' suggestions are
*future* *directions*, which are more to show how flexible the design is
rather than actually be a part of the implementation.

···

On Thu, Jul 14, 2016 at 3:30 PM Charles Srstka via swift-evolution < swift-evolution@swift.org> wrote:

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 <daniel.j.stenmark@gmail.com> > 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 < > swift-evolution@swift.org> 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
>> swift-evolution@swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

--
Dan Appel


(David Waite) #8

The issue with bringing this up now is that much of the core team is distracted by the work of finalizing the Swift 3 implementation, and won’t be able to fully participate.

I have a ton of questions about how opinionated the language will be with regards to concurrency approaches (I’m in the camp that believes Erlang, Go and Node.js have shown opinionated approaches are needed for widespread adoption of concurrency within a language). I’m waiting patiently :slight_smile:

-DW

···

On Jul 14, 2016, at 4:57 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

Is there a way I can make that clearer than putting it in a section labeled “FUTURE DIRECTIONS”?

Charles

On Jul 14, 2016, at 5:52 PM, Dan Appel <dan.appel00@gmail.com <mailto:dan.appel00@gmail.com>> wrote:

Yes, it should be made clear that the 'async'/'await' suggestions are future directions, which are more to show how flexible the design is rather than actually be a part of the implementation.

On Thu, Jul 14, 2016 at 3:30 PM Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
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 <daniel.j.stenmark@gmail.com <mailto:daniel.j.stenmark@gmail.com>> 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 <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> 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
>> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
--
Dan Appel

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Charles Srstka) #9

Is there a way I can make that clearer than putting it in a section labeled “FUTURE DIRECTIONS”?

Charles

···

On Jul 14, 2016, at 5:52 PM, Dan Appel <dan.appel00@gmail.com> wrote:

Yes, it should be made clear that the 'async'/'await' suggestions are future directions, which are more to show how flexible the design is rather than actually be a part of the implementation.

On Thu, Jul 14, 2016 at 3:30 PM Charles Srstka via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
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 <daniel.j.stenmark@gmail.com <mailto:daniel.j.stenmark@gmail.com>> 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 <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> 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
>> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
--
Dan Appel


(Charles Srstka) #10

Won’t that involve source breakage, though? The grammar for calling asynchronous APIs will have changed.

Charles

···

On Jul 14, 2016, at 11:54 PM, Chris Lattner <clattner@apple.com> wrote:

When/if we get an async/await like feature, of course we’ll try to pull completion handlers automatically into the model. We can do that post swift 3.

-Chris

On Jul 14, 2016, at 3:30 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

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 <daniel.j.stenmark@gmail.com> 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 <swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Chris Lattner) #11

Won’t that involve source breakage, though? The grammar for calling asynchronous APIs will have changed.

It depends on the details of the proposal. For example, if the completion handler were sucked into the async model, there is no reason to "take away” the old method. In any case, there is no time to do this in Swift 3, so it is a moot point.

-Chris

···

On Jul 15, 2016, at 10:53 AM, Charles Srstka <cocoadev@charlessoft.com> wrote:

Charles

On Jul 14, 2016, at 11:54 PM, Chris Lattner <clattner@apple.com> wrote:

When/if we get an async/await like feature, of course we’ll try to pull completion handlers automatically into the model. We can do that post swift 3.

-Chris

On Jul 14, 2016, at 3:30 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org> wrote:

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 <daniel.j.stenmark@gmail.com> 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 <swift-evolution@swift.org> 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
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Karl) #12

Realistically, there are still going to be large changes to Swift.
  
I expect conditional conformances, for example, to basically transform huge amounts of Swift code, including the standard library.
  
Karl

···

  
On Jul 15, 2016 at 7:53 PM, <Charles Srstka via swift-evolution (mailto:swift-evolution@swift.org)> wrote:
  
Won’t that involve source breakage, though? The grammar for calling asynchronous APIs will have changed.

Charles

> On Jul 14, 2016, at 11:54 PM, Chris Lattner <clattner@apple.com (mailto:clattner@apple.com)> wrote:
>
> When/if we get an async/await like feature, of course we’ll try to pull completion handlers automatically into the model. We can do that post swift 3.
>
> -Chris
>
>> On Jul 14, 2016, at 3:30 PM, Charles Srstka via swift-evolution <swift-evolution@swift.org (mailto:swift-evolution@swift.org)> wrote:
>>
>> 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 <daniel.j.stenmark@gmail.com (mailto:daniel.j.stenmark@gmail.com)> 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 <swift-evolution@swift.org (mailto:swift-evolution@swift.org)> 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
>>>> swift-evolution@swift.org (mailto:swift-evolution@swift.org)
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution@swift.org (mailto:swift-evolution@swift.org)
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>

_______________________________________________
swift-evolution mailing list (mailto:listswift-evolution@swift.orghttps)
swift-evolution@swift.org (mailto:listswift-evolution@swift.orghttps)
https (mailto:listswift-evolution@swift.orghttps)://lists.swift.org/mailman/listinfo/swift-evolution