[swift-evolution] API Guidelines for Asynchronous Completion Handlers?

Stephen Celis stephen.celis at gmail.com
Fri Dec 4 13:52:54 CST 2015


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 at 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 at 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 at 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 at 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 at apple.com> wrote:
> >>>>>
> >>>>>
> >>>>>> On Dec 3, 2015, at 2:15 PM, Douglas Gregor <dgregor at apple.com>
> wrote:
> >>>>>>
> >>>>>> On Dec 3, 2015, at 12:32 PM, Dan Stenmark <
> daniel.j.stenmark at 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 at swift.org
> >>>> https://lists.swift.org/mailman/listinfo/swift-evolution
> >>>
> >>> _______________________________________________
> >>> swift-evolution mailing list
> >>> swift-evolution at swift.org
> >>> https://lists.swift.org/mailman/listinfo/swift-evolution
> >> _______________________________________________
> >> swift-evolution mailing list
> >> swift-evolution at swift.org
> >> https://lists.swift.org/mailman/listinfo/swift-evolution
> >
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151204/e589cb4e/attachment.html>


More information about the swift-evolution mailing list