With the PromiseKit library, it’s possible to create a promise and a resolver function together and store them on an instance of a class:
class ExampleClass {
// Promise and resolver for the top news headline, as obtained from
// some web service.
private let (headlinePromise, headlineSeal) = Promise<String>.pending()
}
Like any promise, we can chain off of headlinePromise to do some work once the value is available:
headlinePromise.get { headline in
updateUI(headline: headline)
}
// Some other stuff here
Since the promise has not been resolved yet, the contents of the get closure will be enqueued somewhere and control will immediately move to the “some other stuff here” section; updateUI will not be called unless and until the promise is resolved.
To resolve the promise, an instance method can call headlineSeal:
makeNetworkRequest("https://news.example/headline").get { headline in
headlineSeal.fulfill(headline)
}
The promise is now resolved, and any promise chains that had been waiting for headlinePromise will continue. For the rest of the life of this ExampleClass instance, any promise chain starting like
headlinePromise.get { headline in
// ...
}
will immediately begin executing. (“Immediately” might mean “right now, synchronously,” or it might mean “on the next run of the event loop”; the distinction isn’t important for me here.) Since promises can only be resolved once, any future calls to headlineSeal.fulfill(_:) or headlineSeal.reject(_:) will be no-ops.
Question
How can this pattern be translated idiomatically into Swift concurrency (“async/await”)? It’s not important that there be an object called a “promise” and a function called a “resolver”; what I’m looking for is a setup that has the following properties:
- It’s possible for some code to declare a dependency on some bit of asynchronously-available state, and yield until that state is available.
- It’s possible for that state to be “fulfilled” from potentially any instance method.
- Once the state is available, any future chains of code that depend on that state are able to run right away.
- Once the state is available, its value is immutable; the state cannot become unavailable again, nor can its value be changed.
I think that some of these can be accomplished by storing an instance variable
private let headlineTask: Task<String, Error>
and then waiting for the value with
let headline = try await headlineTask.value
but I’m not sure how that Task should be initialized or how it should be “fulfilled.”
Here is a way to reproduce a
Promisewhich can be awaited by multiple consumers and fulfilled by any synchronous code:The
ManagedCriticalStatetype can be found in this file from the SwiftAsyncAlgorithms package.I think I got the implementation safe and correct but if someone finds an error I'll update the answer. For reference I got inspired by
AsyncChanneland this blog post.You can use it like this:
Short explanation
In Swift Concurrency, the high-level
Tasktype resembles a Future/Promise (it can be awaited and suspends execution until resolved) but the actual resolution cannot be controlled from the outside: one must compose built-in lower-level asynchronous functions such asURLSession.data()orTask.sleep().However, Swift Concurrency provides a
(Checked|Unsafe)Continuationtype which basically act as a Promise resolver. It is a low-lever building block which purpose is to migrate regular asynchronous code (callback-based for instance) to the Swift Concurrency world.In the above code, continuations are created by the consumers (via the
.valueproperty) and stored in thePromise. Later, when the result is available the stored continuations are fulfilled (with.resume()), which resumes the execution of the consumers. The result is also cached so that if it is already available when.valueis called it is directly returned to the called.When a
Promiseis fulfilled multiple times, the current behavior is to ignore subsequent calls and to return aa boolean value indicating if thePromisewas already fulfilled. Other API's could be used (a trap, throwing an error, etc.).The internal mutable state of the
Promisemust be protected from concurrent accesses since multiple concurrency domains could try to read and write from it at the same time. This is achieve with regular locking (I believe this could have been achieved with anactor, though).