Convenience asynchronous completion handlers

This pitch proposes that the standard library be amended to contain convenience functions for the most common types of asynchronous completion handlers.

withUnsafeContinuation et al are very flexible and useful, but most use cases are in a very confined area:

func getFoo(..., completion: @escaping (Foo,...)->Void)
func getFoo(..., completion: @escaping (Result<Foo,Error>)->Void)
func getFoo(..., completion: @escaping (Foo?,Error?)->Void) // Foo xor Error
func getFoo(..., completion: @escaping (Foo?,Error?)->Void) // Foo and Error

I have attached a draft of the functions here: GitHub - jjrscott/AsyncCompletionHandler: Generate convenience async completion handlers.

A counter argument is that completion handlers are on the way out so why put transitory functions in the Standard Library. I honestly don't know if they'll stay or go :man_shrugging:t2:

Hi @jjrscott! You may or may not be aware that you can pass continuation.resume directly where an @escaping completion is needed. For example:

func getFoo(..., completion completionHandler: @escaping (Result<Foo, Error>) -> Void)

func foo(...) async throws -> Foo {
  return try await withCheckedThrowingContinuation { cc in
    getFoo(..., completion: cc.resume)
  }
}

It's certainly possible that the Swift team could add additional overloads of resume() that fit other common patterns. However, those patterns tend to be of the sort encountered in idiomatic Objective-C rather than idiomatic Swift, and the compiler already provides automatic translation to async functions for the most common cases:

- (void)twiddleWidget:(NSWidget *)widget completionHandler:(void (^)(NSString *_Nullable result, NSError *_Nullable error))completionHandler;
- (void)becomeMayorOfCity:(City *)city completionHandler:(void (^)(NSError *_Nullable error))completionHandler;
// The above methods are automatically imported as:

func twiddle(_ widget: Widget, completionHandler: @escaping (String?, Error?) -> Void)
func twiddle(_ widget: Widget) async throws -> String
func becomeMayor(of city: City, completionHandler: @escaping (Error?) -> Void)
func becomeMayor(of city: City) async throws -> Void

Can you give some examples of common completion handler patterns that aren't handled well today?

2 Likes

Darn, thanks @grynspan for pointing that out :man_facepalming:t2:

I think the main class of useful wrappers would be these primitives:

func with[Unsafe]CompletionHandler<T,U,V>(_ body: (_ completionHandler: @escaping (T,U,V) -> ()) -> ()) async -> (T,U,V)

to directly map completion handlers to async without worrying about errors or creating a composite type or tuple to get the values back. Everything else can be build/hacked on to of those.

A convenience over a 3ple is hard to justify adding to the standard library—it's trivial to create such a function from existing API since the return type of a continuation can itself be a 3ple:

func withCompletionHandler<T,U,V>(_ body: (_ completionHandler: @escaping (T,U,V) -> ()) -> ()) async -> (T,U,V) {
  return await withCheckedContinuation { cc in
    body { t, u, v in
      cc.resume(returning: (t, u, v))
    }
  }
}

In use:

func spinBiscuits() async -> (T, U, V) {
  return await withCompletionHandler { completionHandler in
    spinBiscuits(completion: completionHandler)
  }
}

But compare that to what is currently required:

func spinBiscuits() async -> (T, U, V) {
  return await withCheckedContinuation { cc in
    spinBiscuits { t, u, v in
      cc.resume(returning: (t, u, v))
    }
  }
}

To my eyes, at least, the new function you're suggesting doesn't make for a more expressive interface. It does allow passing completionHandler directly to the hypothetical spinBiscuits(completion:) function, but that's only possible if the signatures match exactly.

Perhaps I'm misunderstanding what you're suggesting, or maybe there's a use case I'm not seeing where withCompletionHandler(_:) presents a significant improvement over the current API?

1 Like