Earlier in this series, you learned how a subject can bridge the gap between imperative programming and reactive programming. While subjects are convenient in many ways, they are not always the best option. The Combine framework provides another option. In this episode, we zoom in on futures and promises to bridge the gap between imperative programming and reactive programming.
What Are Futures and Promises?
Before I show you how to use futures and promises, I want to clarify what they are. Futures and promises are not unique to the Combine framework. They are sometimes used interchangeably, which adds to the confusion.
A future is a publisher. Future is a final class that conforms to the Publisher protocol. Like any other publisher, a future defines an Output type and a Failure type.
A promise is nothing more than a type alias for a closure that accepts a single argument of type Result and returns Void. The type of the associated value of the success case is the same as the future's Output type and the type of the associated value of the failure case is the same as the future's Failure type.
A future executes its promise when it completes, successfully or unsuccessfully. It emits a single value and a completion event if it completes successfully. It emits an error if it completes unsuccessfully. Don't worry if this sounds confusing. We take a look at an example in a few moments.
When to Use Futures?
When is it appropriate to use a future instead of a subject? Completion handlers are a common use case for futures. Let's take a look at an example. We start with a playground that includes import statements for UIKit and Combine. The playground defines a variable, subscriptions, of type Set<AnyCancellable>. The initial value of subscriptions is an empty set. This should look familiar.
import UIKit
import Combine
var subscriptions: Set<AnyCancellable> = []
We define a function, fetchImage(for:completion:), that accepts a URL as its first argument and a completion handler as its second argument. The completion handler, a closure, accepts a single argument of type Result. The associated value of the result's success case is of type UIImage? and the associated value of the result's failure case is of type Error.
func fetchImage(for url: URL, completion: @escaping (Result<UIImage?, Error>) -> Void) {
}
The fetchImage(for:completion:) function uses the URLSession API to fetch the data for the image the URL points to. We create a data task, passing in the URL of the remote image and a completion handler. The completion handler of the data task accepts an optional Data object, an optional URLResponse object, and an optional Error object. We are only interested in the Data object and the Error object.
func fetchImage(for url: URL, completion: @escaping (Result<UIImage?, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
}
}
In the completion handler of the data task, we safely unwrap the Data object and use it to create a UIImage instance. We execute the completion handler, passing in success as the argument. The associated value of the success case is the UIImage instance.
We use an else if clause to safely unwrap the Error object. We execute the completion handler, passing in failure as the argument. The associated value of the failure case is the Error object. We initiate the data task by invoking its resume() function.
func fetchImage(for url: URL, completion: @escaping (Result<UIImage?, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data {
completion(.success(UIImage(data: data)))
} else if let error = error {
completion(.failure(error))
}
}.resume()
}
The fetchImage(for:completion:) function asynchronously fetches a remote image given a URL. The completion handler notifies the caller of the fetchImage(for:completion:) function that the task completed, successfully or unsuccessfully. While this pattern is useful and commonly used for asynchronous tasks, completion handlers can result in complex code, especially if they are nested several levels deep.
Futures are a powerful alternative to completion handlers with a number of additional benefits. Let's refactor the fetchImage(for:completion:) to use a future instead of a completion handler.
The fetchImage(for:completion:) function no longer returns Void. It returns a future instead. The future's Output type is UIImage? and its Failure type is Error.
func fetchImage(for url: URL, completion: @escaping (Result<UIImage?, Error>) -> Void) -> Future<UIImage?, Error> {
...
}
In the body of the fetchImage(for:completion:) function, we create the future, an instance of the Future class. The future's initializer accepts a closure as an argument. The closure that is passed to the future's initializer accepts a single argument, a promise. The promise is executed when the asynchronous task completes. We make use of trailing closure syntax to keep the code we write simple and readable.
func fetchImage(for url: URL, completion: @escaping (Result<UIImage?, Error>) -> Void) -> Future<UIImage?, Error> {
// Create Future
Future { promise in
}
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data {
completion(.success(UIImage(data: data)))
} else if let error = error {
completion(.failure(error))
}
}.resume()
}
Remember that a promise is a type alias. A promise is a closure that accepts a single argument of type Result and returns Void. The type of the associated value of the success case is the same as the future's Output type and the type of the associated value of the failure case is the same as the future's Failure type. Notice that the parameter of the completion handler of the fetchImage(for:completion:) function is of the same type.
In the closure that is passed to the future's initializer, we create a data task to fetch the data for the image the URL points to. The body of the closure is identical to the original implementation of the fetchImage(for:completion:) function. The difference is that the promise is executed instead of the completion handler. The argument that is passed to the promise, a Result object, is identical to the argument that was passed to the completion handler in the original implementation. We can remove the completion handler from the function signature since the fetchImage(for:) function now uses a future instead of a completion handler.
func fetchImage(for url: URL) -> Future<UIImage?, Error> {
// Create Future
Future { promise in
// Request Data for Image
URLSession.shared.dataTask(with: url) { data, _, error in
if let data = data {
// Execute Promise
promise(.success(UIImage(data: data)))
} else if let error = error {
// Execute Promise
promise(.failure(error))
}
}.resume()
}
}
We successfully replaced the completion handler with a future. Let me show you how to use the refactored fetchImage(for:) function. We invoke the fetchImage(for:) function, passing it the URL of a remote image. The fetchImage(for:) function returns immediately. It no longer accepts a completion handler. It returns a future instead.
As I mentioned earlier, a future is a publisher. We can subscribe to the future by invoking the sink(receiveCompletion:receiveValue:) method. In the completion closure, we print the Completion object to the console. In the value closure, we print the size of the image to the console. We add the subscription to the set of subscriptions.
let url = URL(string: "https://cdn.cocoacasts.com/3cbae1d5e178606580518a81da69e5af30a7bb5b/image-0.jpg")!
fetchImage(for: url)
.sink { completion in
print(completion)
} receiveValue: { image in
print(image!.size)
}
.store(in: &subscriptions)
Execute the contents of the playground. After a few seconds the size of the image is printed to the console followed by the Completion object, indicating that the publisher finished. Remember that a future publishes a single value followed by a completion event if the task completes successfully. It emits an error if the task completes unsuccessfully.
(4096.0, 2304.0)
finished
Traits of a Future
At first glance, a future looks and feels very much like any other publisher. This isn't true, though. There are a number of key differences you need to be aware of.
First, a future emits a single value or it emits an error. If the future emits a single value, that event is immediately followed by a completion event.
Second, Future is a class, a reference type, and that isn't a coincidence. A future publishes the same result to every subscriber that is attached to the future. The closure that is passed to the initializer of the future is executed once. The future holds on to the result and delivers it to every subscriber that is attached to the future. We can verify this by adding a print statement to the closure that is passed to the initializer.
func fetchImage(for url: URL) -> Future<UIImage?, Error> {
// Create Future
Future { promise in
print("execute closure")
...
}
}
We store the future the fetchImage(for:) function returns in a constant, future.
let future = fetchImage(for: url)
We subscribe to the future twice and inspect the output in the console.
future
.sink { completion in
print(completion)
} receiveValue: { image in
print(image!.size)
}
.store(in: &subscriptions)
future
.sink { completion in
print(completion)
} receiveValue: { image in
print(image!.size)
}
.store(in: &subscriptions)
The output confirms that the closure that is passed to the future's initializer is executed once. The output also confirms that the result is replayed to every subscriber that is attached to the future.
execute closure
(4096.0, 2304.0)
finished
(4096.0, 2304.0)
finished
Third, the closure that is passed to the initializer of the future is executed immediately, even if there are no subscribers attached to the future. This isn't true for other publishers. A publisher publishes its first event if at least one subscriber is attached to the publisher. This isn't true for a future. The closure that is passed to the future's initializer is executed immediately, even if no subscribers are attached to the future. We can verify this by removing the subscribers from the future.
execute closure
The output confirms that the data of the remote image is fetched as soon as the future is created by the fetchImage(for:) function even though no subscribers are attached to the future. Take a moment to let this sink in. It is essential that you understand what sets futures apart from other publishers.
Futures and Subjects
Futures and subjects both bridge the gap between imperative programming and reactive programming. Because futures have several unique traits, you need to be mindful when to use them. I already mentioned that completion handlers are a common use case for futures.
Subjects are helpful if a closure is executed multiple times. A handler that is stored as a property is a common use case. You can replace the handler with a subject, wrap the subject in a type eraser, and expose it as AnyPublisher to hide the subject.
Sharing Output
A future holds on to the result of the task and replays that result to every subscriber that is attached to the future. This is unique to futures, but you can replicate this behavior by applying Combine's share operator. We cover the share operator later in this series.
Sharing the result can be useful to save resources, but it can also result in hard to find bugs. It is therefore important to only use a future in scenarios where sharing the result is the desired and expected behavior.
What's Next?
Futures and promises are easy to use once you understand what they are and when they should be used. In the next episode, we use futures and promises in a simple application that relies on completion handlers.