API Guidelines for Asynchronous Completion Handlers?


(Dan Stenmark) #1

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

Dan


(Douglas Gregor) #2

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.

  - Doug

···

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?


(David Hart) #3

A couple of points to think about:

- Seems to me like Swift's error handling should be discussed at the same time because asynchronous callbacks is the only case where we need to revert back to using NSError instead of using Swift’s error handling. What can the language do for us?

- This is slightly off-topic, but do we profit from Swift as a new language to move to a completely different paradigm like Promises?

David

···

On 03 Dec 2015, at 23:15, Douglas Gregor <dgregor@apple.com> wrote:

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com <mailto:daniel.j.stenmark@gmail.com>> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.

  - Doug

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


(Alex Migicovsky) #4

Hi Dan!

I think guidelines in this area would be great.

Here are the tradeoffs I think we have for each approach:

1) The single block approach means you’d code against an optional result and an optional error, making it easy to write invalid code (see example in #2). With the single block you can use trailing closure syntax coherently. I think most ObjC APIs use this approach since it works well in ObjC.

2) As Doug mentioned, the double block can be inconvenient / awkward but it does produce more correct code.

Doug: maybe we can limit using trailing closures from being used if the 2nd to last parameter is also a closure? That would eliminate some confusion at the call site.

Some ObjC APIs use this approach. One positive aspect of the double-block approach is that it always produces code that’s less indented than the single block approach. e.g.

     /*
          Single block.
          Trailing closure syntax works well.
     */
     request.fetch { result, error in
          // More indented code since we need to use guard or if let.
          guard let result = result else {
               // Need to force unwrap `error`.
               handleFetchError(error!)
               return
          }

          use(result)
     }

     /*
          Double block.
          Trailing closure syntax is awkward here.
     */
     request.fetch(withCompletionHandler: { result in
          use(result)
     }, errorHandler: { error in
          // Don’t need to force unwrap `error`.
          handleFetchError(error)
     })

3) Enums with associated values are conceptually nice, but unless we have a Result<> or an Either<> in the Standard Library I think most people will write one-off enums for each set of methods that return a specific kind of result + error. That adds an unnecessary conceptual burden since you need to know the type of of the value that’s passed to each callback. Also, we don’t have any primarily ObjC APIs that use this approach yet. It would also suffer from the same indentation problem as #1 but without the “invalid code” problem. If we go this route I think we’d want to map the async error ObjC APIs to use this approach similar to what we do with non-async error handling.

Looking at the tradeoffs I think I prefer #2 if we could limit the ability to use a trailing closure for the last parameter. I’d want to look at more code with the change though. We should also consider whether we should map the single block APIs in ObjC into double block APIs. What do you think?

Also, with any of these approaches there’s also the question of whether we pass ErrorType, NSError, or the specific error type.

- Alex

···

On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor@apple.com> wrote:

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com <mailto:daniel.j.stenmark@gmail.com>> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.


(Sean Heber) #5

What if there was a possibility for a deferred throw?

For example, imagine the fetch function was something like this:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {}

And to call it, you’d wrap it in a try with a new kind of catch:

do {
  try request.fetch() { result in … }
} defer catch (… pattern) {
}

So what the language would do then is treat all of the “defer catch” blocks as closures and pass them along with calls to “defer throws” functions in the same context. When they throw, they would then run those blocks as if you had passed them along manually in the double block style.

The context to throw back to would need to be capture-able by the function body for fetch() so that if it encounters an error some time later it would know where to throw it to. Perhaps the easiest way would be to create a closure that throws like so:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {
  var context = FetchContext()
  context.completionHandler = withCompletionHandler
  context.errorHandler = { reason in throw RequestError(reason) }
  self.pending.append(context)
  context.start()
}

Under the hood the “throw” captured in the closure would also be carrying along the context needed to route that error back to the expected defer catch block.

l8r
Sean

···

On Dec 4, 2015, at 10:57 AM, Alex Migicovsky <migi@apple.com> wrote:

On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor@apple.com> wrote:

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.

Hi Dan!

I think guidelines in this area would be great.

Here are the tradeoffs I think we have for each approach:

1) The single block approach means you’d code against an optional result and an optional error, making it easy to write invalid code (see example in #2). With the single block you can use trailing closure syntax coherently. I think most ObjC APIs use this approach since it works well in ObjC.

2) As Doug mentioned, the double block can be inconvenient / awkward but it does produce more correct code.

Doug: maybe we can limit using trailing closures from being used if the 2nd to last parameter is also a closure? That would eliminate some confusion at the call site.

Some ObjC APIs use this approach. One positive aspect of the double-block approach is that it always produces code that’s less indented than the single block approach. e.g.

     /*
          Single block.
          Trailing closure syntax works well.
     */
     request.fetch { result, error in
          // More indented code since we need to use guard or if let.
          guard let result = result else {
               // Need to force unwrap `error`.
               handleFetchError(error!)
               return
          }

          use(result)
     }

     /*
          Double block.
          Trailing closure syntax is awkward here.
     */
     request.fetch(withCompletionHandler: { result in
          use(result)
     }, errorHandler: { error in
          // Don’t need to force unwrap `error`.
          handleFetchError(error)
     })

3) Enums with associated values are conceptually nice, but unless we have a Result<> or an Either<> in the Standard Library I think most people will write one-off enums for each set of methods that return a specific kind of result + error. That adds an unnecessary conceptual burden since you need to know the type of of the value that’s passed to each callback. Also, we don’t have any primarily ObjC APIs that use this approach yet. It would also suffer from the same indentation problem as #1 but without the “invalid code” problem. If we go this route I think we’d want to map the async error ObjC APIs to use this approach similar to what we do with non-async error handling.

Looking at the tradeoffs I think I prefer #2 if we could limit the ability to use a trailing closure for the last parameter. I’d want to look at more code with the change though. We should also consider whether we should map the single block APIs in ObjC into double block APIs. What do you think?

Also, with any of these approaches there’s also the question of whether we pass ErrorType, NSError, or the specific error type.

- Alex

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


(Vinicius Vendramini) #6

I really like the idea of using try/catches, it seems like a more swifty approach. However, I'd prefer to do it in a way that doesn't add a new syntax.

Also, I agree with Alex, the second options seems the best of those alternatives (if it were done without trailing closures.)

···

On Dec 4, 2015, at 12:21 PM, Sean Heber <sean@fifthace.com> wrote:

What if there was a possibility for a deferred throw?

For example, imagine the fetch function was something like this:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {}

And to call it, you’d wrap it in a try with a new kind of catch:

do {
try request.fetch() { result in … }
} defer catch (… pattern) {
}

So what the language would do then is treat all of the “defer catch” blocks as closures and pass them along with calls to “defer throws” functions in the same context. When they throw, they would then run those blocks as if you had passed them along manually in the double block style.

The context to throw back to would need to be capture-able by the function body for fetch() so that if it encounters an error some time later it would know where to throw it to. Perhaps the easiest way would be to create a closure that throws like so:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {
var context = FetchContext()
context.completionHandler = withCompletionHandler
context.errorHandler = { reason in throw RequestError(reason) }
self.pending.append(context)
context.start()
}

Under the hood the “throw” captured in the closure would also be carrying along the context needed to route that error back to the expected defer catch block.

l8r
Sean

On Dec 4, 2015, at 10:57 AM, Alex Migicovsky <migi@apple.com> wrote:

On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor@apple.com> wrote:

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.

Hi Dan!

I think guidelines in this area would be great.

Here are the tradeoffs I think we have for each approach:

1) The single block approach means you’d code against an optional result and an optional error, making it easy to write invalid code (see example in #2). With the single block you can use trailing closure syntax coherently. I think most ObjC APIs use this approach since it works well in ObjC.

2) As Doug mentioned, the double block can be inconvenient / awkward but it does produce more correct code.

Doug: maybe we can limit using trailing closures from being used if the 2nd to last parameter is also a closure? That would eliminate some confusion at the call site.

Some ObjC APIs use this approach. One positive aspect of the double-block approach is that it always produces code that’s less indented than the single block approach. e.g.

    /*
         Single block.
         Trailing closure syntax works well.
    */
    request.fetch { result, error in
         // More indented code since we need to use guard or if let.
         guard let result = result else {
              // Need to force unwrap `error`.
              handleFetchError(error!)
              return
         }

         use(result)
    }

    /*
         Double block.
         Trailing closure syntax is awkward here.
    */
    request.fetch(withCompletionHandler: { result in
         use(result)
    }, errorHandler: { error in
         // Don’t need to force unwrap `error`.
         handleFetchError(error)
    })

3) Enums with associated values are conceptually nice, but unless we have a Result<> or an Either<> in the Standard Library I think most people will write one-off enums for each set of methods that return a specific kind of result + error. That adds an unnecessary conceptual burden since you need to know the type of of the value that’s passed to each callback. Also, we don’t have any primarily ObjC APIs that use this approach yet. It would also suffer from the same indentation problem as #1 but without the “invalid code” problem. If we go this route I think we’d want to map the async error ObjC APIs to use this approach similar to what we do with non-async error handling.

Looking at the tradeoffs I think I prefer #2 if we could limit the ability to use a trailing closure for the last parameter. I’d want to look at more code with the change though. We should also consider whether we should map the single block APIs in ObjC into double block APIs. What do you think?

Also, with any of these approaches there’s also the question of whether we pass ErrorType, NSError, or the specific error type.

- Alex

_______________________________________________
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 Stenmark) #7

Hey Alex!

I completely agree that the double-block approach is advantageous for its reduced unwrapping, but the only issue I have is that, when applied in production, it typically leads to duplicate code.

request.fetch(withCompletionHandler: { result in
          activityIndicator.stopAnimating()
          button.enabled = true
          tableView.hidden = false

          use(result)

          print( “Foo!” )

     }, errorHandler: { error in
          activityIndicator.stopAnimating()
          button.enabled = true
          tableView.hidden = false

          // Don’t need to force unwrap `error`.
          handleFetchError(error)

          print( “Foo!” )

     })

The same issue is present in the aforementioned deferred throw concept. There’re certainly ways to mitigate the problem, but no practical way of eliminating it altogether. I’m personally leaning towards an evolution of the Enum-based approach for this very reason, but if a solution can be figured out to remove such code duplication, I’d happily adopt double-blocks.

Dan

···

On Dec 4, 2015, at 10:20 AM, Vinicius Vendramini <vinivendra@gmail.com> wrote:

I really like the idea of using try/catches, it seems like a more swifty approach. However, I'd prefer to do it in a way that doesn't add a new syntax.

Also, I agree with Alex, the second options seems the best of those alternatives (if it were done without trailing closures.)

On Dec 4, 2015, at 12:21 PM, Sean Heber <sean@fifthace.com> wrote:

What if there was a possibility for a deferred throw?

For example, imagine the fetch function was something like this:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {}

And to call it, you’d wrap it in a try with a new kind of catch:

do {
try request.fetch() { result in … }
} defer catch (… pattern) {
}

So what the language would do then is treat all of the “defer catch” blocks as closures and pass them along with calls to “defer throws” functions in the same context. When they throw, they would then run those blocks as if you had passed them along manually in the double block style.

The context to throw back to would need to be capture-able by the function body for fetch() so that if it encounters an error some time later it would know where to throw it to. Perhaps the easiest way would be to create a closure that throws like so:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {
var context = FetchContext()
context.completionHandler = withCompletionHandler
context.errorHandler = { reason in throw RequestError(reason) }
self.pending.append(context)
context.start()
}

Under the hood the “throw” captured in the closure would also be carrying along the context needed to route that error back to the expected defer catch block.

l8r
Sean

On Dec 4, 2015, at 10:57 AM, Alex Migicovsky <migi@apple.com> wrote:

On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor@apple.com> wrote:

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.

Hi Dan!

I think guidelines in this area would be great.

Here are the tradeoffs I think we have for each approach:

1) The single block approach means you’d code against an optional result and an optional error, making it easy to write invalid code (see example in #2). With the single block you can use trailing closure syntax coherently. I think most ObjC APIs use this approach since it works well in ObjC.

2) As Doug mentioned, the double block can be inconvenient / awkward but it does produce more correct code.

Doug: maybe we can limit using trailing closures from being used if the 2nd to last parameter is also a closure? That would eliminate some confusion at the call site.

Some ObjC APIs use this approach. One positive aspect of the double-block approach is that it always produces code that’s less indented than the single block approach. e.g.

   /*
        Single block.
        Trailing closure syntax works well.
   */
   request.fetch { result, error in
        // More indented code since we need to use guard or if let.
        guard let result = result else {
             // Need to force unwrap `error`.
             handleFetchError(error!)
             return
        }

        use(result)
   }

   /*
        Double block.
        Trailing closure syntax is awkward here.
   */
   request.fetch(withCompletionHandler: { result in
        use(result)
   }, errorHandler: { error in
        // Don’t need to force unwrap `error`.
        handleFetchError(error)
   })

3) Enums with associated values are conceptually nice, but unless we have a Result<> or an Either<> in the Standard Library I think most people will write one-off enums for each set of methods that return a specific kind of result + error. That adds an unnecessary conceptual burden since you need to know the type of of the value that’s passed to each callback. Also, we don’t have any primarily ObjC APIs that use this approach yet. It would also suffer from the same indentation problem as #1 but without the “invalid code” problem. If we go this route I think we’d want to map the async error ObjC APIs to use this approach similar to what we do with non-async error handling.

Looking at the tradeoffs I think I prefer #2 if we could limit the ability to use a trailing closure for the last parameter. I’d want to look at more code with the change though. We should also consider whether we should map the single block APIs in ObjC into double block APIs. What do you think?

Also, with any of these approaches there’s also the question of whether we pass ErrorType, NSError, or the specific error type.

- Alex

_______________________________________________
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

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


(Sean Heber) #8

I think that the code duplication issue is more of an API design side effect. If the API was designed with a 3rd block for when it is “done” regardless of error, then you could put that shared code there - but I would imagine that’d be out of the scope of Swift itself. What I often do in these situations is just make a local function that does the shared stuff and then call it from both blocks:

func finished() {
          activityIndicator.stopAnimating()
          button.enabled = true
          tableView.hidden = false
}

request.fetch(withCompletionHandler: { result in
          finished()
          use(result)
     }, errorHandler: { error in
          finished()
          handleFetchError(error)
     })

l8r
Sean

···

On Dec 4, 2015, at 1:13 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> wrote:

Hey Alex!

I completely agree that the double-block approach is advantageous for its reduced unwrapping, but the only issue I have is that, when applied in production, it typically leads to duplicate code.

request.fetch(withCompletionHandler: { result in
          activityIndicator.stopAnimating()
          button.enabled = true
          tableView.hidden = false

          use(result)

          print( “Foo!” )

     }, errorHandler: { error in
          activityIndicator.stopAnimating()
          button.enabled = true
          tableView.hidden = false

          // Don’t need to force unwrap `error`.
          handleFetchError(error)

          print( “Foo!” )

     })

The same issue is present in the aforementioned deferred throw concept. There’re certainly ways to mitigate the problem, but no practical way of eliminating it altogether. I’m personally leaning towards an evolution of the Enum-based approach for this very reason, but if a solution can be figured out to remove such code duplication, I’d happily adopt double-blocks.

Dan

On Dec 4, 2015, at 10:20 AM, Vinicius Vendramini <vinivendra@gmail.com> wrote:

I really like the idea of using try/catches, it seems like a more swifty approach. However, I'd prefer to do it in a way that doesn't add a new syntax.

Also, I agree with Alex, the second options seems the best of those alternatives (if it were done without trailing closures.)

On Dec 4, 2015, at 12:21 PM, Sean Heber <sean@fifthace.com> wrote:

What if there was a possibility for a deferred throw?

For example, imagine the fetch function was something like this:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {}

And to call it, you’d wrap it in a try with a new kind of catch:

do {
try request.fetch() { result in … }
} defer catch (… pattern) {
}

So what the language would do then is treat all of the “defer catch” blocks as closures and pass them along with calls to “defer throws” functions in the same context. When they throw, they would then run those blocks as if you had passed them along manually in the double block style.

The context to throw back to would need to be capture-able by the function body for fetch() so that if it encounters an error some time later it would know where to throw it to. Perhaps the easiest way would be to create a closure that throws like so:

func fetch(withCompletionHandler: (Result) -> ()) defer throws {
var context = FetchContext()
context.completionHandler = withCompletionHandler
context.errorHandler = { reason in throw RequestError(reason) }
self.pending.append(context)
context.start()
}

Under the hood the “throw” captured in the closure would also be carrying along the context needed to route that error back to the expected defer catch block.

l8r
Sean

On Dec 4, 2015, at 10:57 AM, Alex Migicovsky <migi@apple.com> wrote:

On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor@apple.com> wrote:

On Dec 3, 2015, at 12:32 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> wrote:

There’s a some of debate in the community regarding best practice for asynchronous completion callbacks. These practices include:

- Single Block w/ Mutually Exclusive Result and Error Objects (the current standard convention in Cocoa, though originally designed with Objective-C in mind)
- Double Block (one for success, one for failure)
- Swift Enum w/ Associated Objects (as described here: http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/)

Even prior to Swift, Apple’s code guidelines never explicitly addressed this topic. Going forward into the brave new world of Swift, are there going to be new preferred API design guidelines for this?

This is a great point, and there are a number of other issues related to callbacks/closure arguments that would benefit from guidelines. For example, I've seen the “Double Block” case where the second block ends up being a trailing closure, which makes for non-intuitive uses.

Hi Dan!

I think guidelines in this area would be great.

Here are the tradeoffs I think we have for each approach:

1) The single block approach means you’d code against an optional result and an optional error, making it easy to write invalid code (see example in #2). With the single block you can use trailing closure syntax coherently. I think most ObjC APIs use this approach since it works well in ObjC.

2) As Doug mentioned, the double block can be inconvenient / awkward but it does produce more correct code.

Doug: maybe we can limit using trailing closures from being used if the 2nd to last parameter is also a closure? That would eliminate some confusion at the call site.

Some ObjC APIs use this approach. One positive aspect of the double-block approach is that it always produces code that’s less indented than the single block approach. e.g.

   /*
        Single block.
        Trailing closure syntax works well.
   */
   request.fetch { result, error in
        // More indented code since we need to use guard or if let.
        guard let result = result else {
             // Need to force unwrap `error`.
             handleFetchError(error!)
             return
        }

        use(result)
   }

   /*
        Double block.
        Trailing closure syntax is awkward here.
   */
   request.fetch(withCompletionHandler: { result in
        use(result)
   }, errorHandler: { error in
        // Don’t need to force unwrap `error`.
        handleFetchError(error)
   })

3) Enums with associated values are conceptually nice, but unless we have a Result<> or an Either<> in the Standard Library I think most people will write one-off enums for each set of methods that return a specific kind of result + error. That adds an unnecessary conceptual burden since you need to know the type of of the value that’s passed to each callback. Also, we don’t have any primarily ObjC APIs that use this approach yet. It would also suffer from the same indentation problem as #1 but without the “invalid code” problem. If we go this route I think we’d want to map the async error ObjC APIs to use this approach similar to what we do with non-async error handling.

Looking at the tradeoffs I think I prefer #2 if we could limit the ability to use a trailing closure for the last parameter. I’d want to look at more code with the change though. We should also consider whether we should map the single block APIs in ObjC into double block APIs. What do you think?

Also, with any of these approaches there’s also the question of whether we pass ErrorType, NSError, or the specific error type.

- Alex

_______________________________________________
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

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


#9

There's a concurrency proposal that discusses various options here:
https://github.com/apple/swift/blob/master/docs/proposals/Concurrency.rst

I like the idea of using try-catch for asynchronous error handling, but
don't think that defers needs to annotate throws. With a language-level
async/await model we'd get deferred catching for free:

    class Request {
        async func fetch() throws -> Response {}
    }

    try {
        let response = try await request.fetch()
    } catch {
        // handle error
    }

The "try await …" is a bit wordy, though.

···

On Fri, Dec 4, 2015 at 2:19 PM, Sean Heber <sean@fifthace.com> wrote:

I think that the code duplication issue is more of an API design side
effect. If the API was designed with a 3rd block for when it is “done”
regardless of error, then you could put that shared code there - but I
would imagine that’d be out of the scope of Swift itself. What I often do
in these situations is just make a local function that does the shared
stuff and then call it from both blocks:

func finished() {
          activityIndicator.stopAnimating()
          button.enabled = true
          tableView.hidden = false
}

request.fetch(withCompletionHandler: { result in
          finished()
          use(result)
     }, errorHandler: { error in
          finished()
          handleFetchError(error)
     })

l8r
Sean

> On Dec 4, 2015, at 1:13 PM, Dan Stenmark <daniel.j.stenmark@gmail.com> > wrote:
>
> Hey Alex!
>
> I completely agree that the double-block approach is advantageous for
its reduced unwrapping, but the only issue I have is that, when applied in
production, it typically leads to duplicate code.
>
> request.fetch(withCompletionHandler: { result in
> activityIndicator.stopAnimating()
> button.enabled = true
> tableView.hidden = false
>
> use(result)
>
> print( “Foo!” )
>
> }, errorHandler: { error in
> activityIndicator.stopAnimating()
> button.enabled = true
> tableView.hidden = false
>
> // Don’t need to force unwrap `error`.
> handleFetchError(error)
>
> print( “Foo!” )
>
> })
>
> The same issue is present in the aforementioned deferred throw concept.
There’re certainly ways to mitigate the problem, but no practical way of
eliminating it altogether. I’m personally leaning towards an evolution of
the Enum-based approach for this very reason, but if a solution can be
figured out to remove such code duplication, I’d happily adopt
double-blocks.
>
> Dan
>
>> On Dec 4, 2015, at 10:20 AM, Vinicius Vendramini <vinivendra@gmail.com> > wrote:
>>
>> I really like the idea of using try/catches, it seems like a more
swifty approach. However, I'd prefer to do it in a way that doesn't add a
new syntax.
>>
>> Also, I agree with Alex, the second options seems the best of those
alternatives (if it were done without trailing closures.)
>>
>>> On Dec 4, 2015, at 12:21 PM, Sean Heber <sean@fifthace.com> wrote:
>>>
>>> What if there was a possibility for a deferred throw?
>>>
>>> For example, imagine the fetch function was something like this:
>>>
>>> func fetch(withCompletionHandler: (Result) -> ()) defer throws {}
>>>
>>> And to call it, you’d wrap it in a try with a new kind of catch:
>>>
>>> do {
>>> try request.fetch() { result in … }
>>> } defer catch (… pattern) {
>>> }
>>>
>>> So what the language would do then is treat all of the “defer catch”
blocks as closures and pass them along with calls to “defer throws”
functions in the same context. When they throw, they would then run those
blocks as if you had passed them along manually in the double block style.
>>>
>>> The context to throw back to would need to be capture-able by the
function body for fetch() so that if it encounters an error some time later
it would know where to throw it to. Perhaps the easiest way would be to
create a closure that throws like so:
>>>
>>> func fetch(withCompletionHandler: (Result) -> ()) defer throws {
>>> var context = FetchContext()
>>> context.completionHandler = withCompletionHandler
>>> context.errorHandler = { reason in throw RequestError(reason) }
>>> self.pending.append(context)
>>> context.start()
>>> }
>>>
>>> Under the hood the “throw” captured in the closure would also be
carrying along the context needed to route that error back to the expected
defer catch block.
>>>
>>> l8r
>>> Sean
>>>
>>>
>>>>> On Dec 4, 2015, at 10:57 AM, Alex Migicovsky <migi@apple.com> wrote:
>>>>>
>>>>>
>>>>>> On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor@apple.com> > wrote:
>>>>>>
>>>>>> On Dec 3, 2015, at 12:32 PM, Dan Stenmark < > daniel.j.stenmark@gmail.com> wrote:
>>>>>>
>>>>>> There’s a some of debate in the community regarding best practice
for asynchronous completion callbacks. These practices include:
>>>>>>
>>>>>> - Single Block w/ Mutually Exclusive Result and Error Objects (the
current standard convention in Cocoa, though originally designed with
Objective-C in mind)
>>>>>> - Double Block (one for success, one for failure)
>>>>>> - Swift Enum w/ Associated Objects (as described here:
http://www.developerdave.co.uk/2015/09/better-completion-handlers-in-swift/
)
>>>>>>
>>>>>> Even prior to Swift, Apple’s code guidelines never explicitly
addressed this topic. Going forward into the brave new world of Swift, are
there going to be new preferred API design guidelines for this?
>>>>>
>>>>> This is a great point, and there are a number of other issues
related to callbacks/closure arguments that would benefit from guidelines.
For example, I've seen the “Double Block” case where the second block ends
up being a trailing closure, which makes for non-intuitive uses.
>>>>
>>>> Hi Dan!
>>>>
>>>> I think guidelines in this area would be great.
>>>>
>>>> Here are the tradeoffs I think we have for each approach:
>>>>
>>>> 1) The single block approach means you’d code against an optional
result and an optional error, making it easy to write invalid code (see
example in #2). With the single block you can use trailing closure syntax
coherently. I think most ObjC APIs use this approach since it works well in
ObjC.
>>>>
>>>> 2) As Doug mentioned, the double block can be inconvenient / awkward
but it does produce more correct code.
>>>>
>>>> Doug: maybe we can limit using trailing closures from being used if
the 2nd to last parameter is also a closure? That would eliminate some
confusion at the call site.
>>>>
>>>> Some ObjC APIs use this approach. One positive aspect of the
double-block approach is that it always produces code that’s less indented
than the single block approach. e.g.
>>>>
>>>> /*
>>>> Single block.
>>>> Trailing closure syntax works well.
>>>> */
>>>> request.fetch { result, error in
>>>> // More indented code since we need to use guard or if let.
>>>> guard let result = result else {
>>>> // Need to force unwrap `error`.
>>>> handleFetchError(error!)
>>>> return
>>>> }
>>>>
>>>> use(result)
>>>> }
>>>>
>>>> /*
>>>> Double block.
>>>> Trailing closure syntax is awkward here.
>>>> */
>>>> request.fetch(withCompletionHandler: { result in
>>>> use(result)
>>>> }, errorHandler: { error in
>>>> // Don’t need to force unwrap `error`.
>>>> handleFetchError(error)
>>>> })
>>>>
>>>> 3) Enums with associated values are conceptually nice, but unless we
have a Result<> or an Either<> in the Standard Library I think most people
will write one-off enums for each set of methods that return a specific
kind of result + error. That adds an unnecessary conceptual burden since
you need to know the type of of the value that’s passed to each callback.
Also, we don’t have any primarily ObjC APIs that use this approach yet. It
would also suffer from the same indentation problem as #1 but without the
“invalid code” problem. If we go this route I think we’d want to map the
async error ObjC APIs to use this approach similar to what we do with
non-async error handling.
>>>>
>>>> Looking at the tradeoffs I think I prefer #2 if we could limit the
ability to use a trailing closure for the last parameter. I’d want to look
at more code with the change though. We should also consider whether we
should map the single block APIs in ObjC into double block APIs. What do
you think?
>>>>
>>>> Also, with any of these approaches there’s also the question of
whether we pass ErrorType, NSError, or the specific error type.
>>>>
>>>> - Alex
>>>>
>>>>
>>>> _______________________________________________
>>>> 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
>> _______________________________________________
>> 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