FutureLib
FutureLib is a pure Swift 2 library implementing Futures & Promises inspired by Scala, Promises/A+ and a cancellation concept with CancellationRequest
and CancellationToken
similar to Cancellation in Managed Threads in Microsoft's Task Parallel Library (TPL).
FutureLib helps you to write concise and comprehensible code to implement correct asynchronous programs which include error handling and cancellation.
Features
- Employs the asynchronous "non-blocking" style.
- Supports composition of tasks.
- Supports a powerful cancellation concept by means of "cancellation tokens".
- Greatly simplifies error handling in asynchronous code.
- Continuations can be specified to run on a certain "Execution Context".
Contents
- Getting Started
- What is a Future?
- What is a Promise?
- Retrieving the Value of the Future
- Basic Methods Registering Continuations
- Combinators
- Sequences of Futures and Extensions to Sequences
- Examples for Combining Futures
- Specify an Execution Context where callbacks will execute
- Cancelling a Continuation
- Wrap an asynchronous function with a completion handler into a function which returns a corresponding future
- Installation
Getting Started
The following sections show how to use futures and promises in short examples.
What is a Future?
A future represents the eventual result of an asynchronous function. Say, the computed value is of type T
, the asynchronous function immediately returns a value of type Future<T>
:
func doSomethingAsync() -> Future<Int>
When the function returns, the returned future is not yet completed - but there executes a background task which computes the value and eventually completes the future. We can say, the returned future is a placeholder for the result of the asynchronous function.
The underlying task may fail. In this case the future will be completed with an error. Note however, that the asynchronous function itself does not throw an error.
A Future is a placeholder for the result of a computation which is not yet finished. Eventually it will be completed with either the value or an error.
In order to represent that kind of result, a future uses an enum type Try<T>
internally. Try
is a kind of variant, or discriminated union which contains either a value or an error. Note, that there are other Swift libraries with a similar type which is usually named Try
. The name Try
is borrowed from Scala.
In FutureLib,
Try<T>
can contain either a value of typeT
or a value conforming to the Swift protocolErrorType
.
Usually, we obtain a future from an asynchronous function like doSomethingAsync()
above. In order to retrieve the result, we register a continuation which gets called when the future completes. However, as a client we cannot complete a future ourself - it's some kind of read-only.
A Future is of read only. We cannot complete it directly, we can only retrieve its result - once it is completed.
So, how does the underlying task complete the future? Well, this will be accomplished with a Promise:
What is a Promise?
With a Promise we can complete a future. Usually, a Promise will be created and eventually resolved by the underlying task. A promise has one and only one associated future. A promise can be resolved with either the computed value or an error. Resolving a promise with a Try
immediately completes its associated future with the same value.
A Promise will be created and resolved by the underlying task. Resolving a Promise immediately completes its Future accordingly.
The sample below shows how to use a promise and how to return its associated future to the caller. In this sample, a function with a completion handler will be wrapped into a function that returns a future:
public func doSomethingAsync -> Future<Int> {
// First, create a promise:
let promise = Promise<Int>()
// Start the asynchronous work:
doSomethingAsyncWithCompletion { (data, error) -> Void in
if let e = error {
promise.reject(e)
}
else {
promise.fulfill(data!)
}
}
// Return the pending future:
return promise.future!
}
Retrieving the Value of the Future
Once we have a future, how do we obtain the value - respectively the error - from the future? And when should we attempt to retrieve it?
Well, it should be clear, that we can obtain the value only after the future has been completed with either the computed value or an error.
There are blocking and non-blocking variants to obtain the result of the future. The blocking variants are rarely used:
Blocking Access
func get() throws -> T
Method value
blocks the current thread until the future is completed. If the future has been completed with success it returns the success value of its result, otherwise it throws the error value. The use of this method is discouraged however since it blocks the current tread. It might be merely be useful in Unit tests or other testing code.
Non-Blocking Access
var result: Try<ValueType>?
If the future is completed returns its result, otherwise it returns nil
. The property is sometimes useful when it's known that the future is already completed.
The most flexible and useful approach to retrieve the result in a non-blocking manner is to use Continuations:
Non-Blocking Access with Continuations
In order to retrieve the result from a future in a non-blocking manner we can use a Continuation. A continuation is a closure which will be registered with certain methods defined for that future. The continuation will be called when the future has been completed.
There are several variants of continuations, including those that are registered with combinators which differ in their signature. Most continuations have a parameter result as Try<T>
, value as T
or error as ErrorType
which will be set accordingly from the future's result and passed as an argument.
Basic Methods Registering Continuations
The most basic method which registers a continuation is onComplete
:
onComplete
func onComplete<U>(f: Try<T> -> U)
Method onComplete
registers a continuation which will be called when the future has been completed. It gets passed the result as a Try<T>
of the future as its argument:
future.onComplete { result in
// result is of type Try<T>
}
where result
is of type Try<T>
where T
is the type of the computed value of the function doSomethingAsync
. result may contain either a value of type T
or an error, conforming to protocol ErrorType
.
Almost all methods which register a continuation are implemented in terms of
onComplete
.
There a few approaches to get the actual value of a result:
let result:Try<Int> = ...
switch result {
case .Success(let value):
print("Value: \(value)")
case .Failure(let error):
print("Error: \(error)")
}
The next basic methods are onSuccess
and onFailure
, which get called when the future completes with success respectively with an error.
onSuccess
func onSuccess<U>(f: T -> throws U)
With method onSuccess
we register a continuation which gets called when the future has been completed with success:
future.onSuccess { value in
// value is of type T
}
onFailure
func onFailure<U>(f: T -> U)
With onFailure
we register a continuation which gets called when the future has been completed with an error:
future.onFailure { error in
// error conforms to protocol `ErrorType`
}
Combinators
Continuations will also be registered with Combinators. A combinator is a method which returns a new future. There are quite a few combinators, most notable map
and flatMap
. There are however quite a few more combinators which build upon the basic ones.
With combinators we can combine two or more futures and build more complex asynchronous patterns and programs.
map
func map<U>(f: T throws -> U) -> Future<U>
Method map
returns a new future which is completed with the result of the function f
which is applied to the success value of self
. If self
has been completed with an error, or if the function f
throws and error, the returned future will be completed with the same error. The continuation will not be called when self
fails.
Since the return type of combinators like map
is again a future we can combine them in various ways. For example:
fetchUserAsync(url).map { user in
print("User: \(user)")
return user.name()
}.map { name in
print("Name: \(name)")
}
.onError { error in
print("Error: \(error)")
}
Note, that the mapping function will be called asynchronously with respect to the caller! In fact the entire expression is asynchronous! Here, the type of the expression above is Void
since onError
returns Void
.
flatMap
func flatMap<U>(f: T throws -> Future<U>) -> Future<U>
Method flatMap
returns a new future which is completed with the eventual result of the function f
which is applied to the success value of self
. If self
has been completed with an error the returned future will be completed with the same error. The continuation will not be called when self
fails.
An example:
fetchUserAsync(url).flatMap { user in
return fetchImageAsync(user.imageUrl)
}.map { image in
dispatch_async(dispatch_get_main_queue()) {
self.image = image
}
}
.onError { error in
print("Error: \(error)")
}
Note: there are simpler ways to specify the execution environment (here the main dispatch queue) where the continuation should be executed.
recover
func recover(f: ErrorType throws -> T) -> Future<T>`
Returns a new future which will be completed with self
's success value or with the return value of the mapping function f
when self
fails.
recoverWith
func recoverWith(f: ErrorType throws -> Future<T>) -> Future<T>
Returns a new future which will be completed with self
's success value or with the deferred result of the mapping function f
when self
fails.
Usually, recover
or recoverWith
will be needed when a subsequent operation will be required to be processed even when the previous task returned an error. We then "recover" from the error by returning a suitable value which may indicate this error or use a default value for example:
let future = computeString().recover { error in
NSLog("Error: \(error)")
return ""
}
filter
func filter(predicate: T throws -> Bool) -> Future<T>
Method filter
returns a new future which is completed with the success value of self
if the function predicate
applied to the value returns true
. Otherwise, the returned future will be completed with the error FutureError.NoSuchElement
. If self
will be completed with an error or if the predicate throws an error, the returned future will be completed with the same error.
computeString().filter { str in
}
transform
func transform<U>(s: T throws -> U, f: ErrorType -> ErrorType)-> Future<U>
Returns a new Future which is completed with the result of function s
applied to the successful result of self
or with the result of function f
applied to the error value of self
. If s
throws an error, the returned future will be completed with the same error.
func transform<U>(f: Try<T> throws -> Try<U>) -> Future<U>
Returns a new Future by applying the specified function to the result of self
. If 'f' throws an error, the returned future will be completed with the same error.
func transformWith<U>(f: Try<T> throws -> Future<U>) -> Future<U>`
Returns a new Future by applying the specified function, which produces a Future, to the result of this Future. If 'f' throws an error, the returned future will be completed with the same error.
zip
func zip(other: Future<U>) -> Future<(T, U)>
Returns a new future which is completed with a tuple of the success value of self
and other
. If self
or other fails with an error, the returned future will be completed with the same error.
Sequences of Futures and Extensions to Sequences
firstCompleted
func firstCompleted() -> Future<T>
Returns a new Future
which will be completed with the result of the first completed future in self
.
traverse
func traverse<U>(task: T throws -> Future<U>) -> Future<[U]>
For any sequence of T
, the asynchronous method traverse
applies the function task
to each value of the sequence (thus, getting a sequence of tasks) and then completes the returned future with an array of U
s once all tasks have been completed successfully.
let ids = [14, 34, 28]
ids.traverse { id in
return fetchUser(id)
}.onSuccess { users in
// user is of type [User]
}
The tasks will be executed concurrently, unless an execution context is specified which defines certain concurrency constraints (e.g., restricting the number of concurrent tasks to a fixed number).
sequence
func sequence() -> Future<[T]>
For a sequence of futures Future<T>
the method sequence
returns a new future Future<[T]>
which is completed with an array of T
, where each element in the array is the success value of the corresponding future in self
in the same order.
[
fetchUser(14),
fetchUser(34),
fetchUser(28)
].sequence { users in
// user is of type [User]
}
results
func results() -> Future<Try<T>>
For a sequence of futures Future<T>
, the method result
returns a new future which is completed with an array of Try<T>
, where each element in the array corresponds to the result of the future in self
in the same order.
[
fetchUser(14),
fetchUser(34),
fetchUser(28)
].results { results in
// results is of type [Try<User>]
}
fold
func fold<U>(initial: U, combine T throws -> U) -> Future<U>
For a sequence of futures Future<T>
returns a new future Future<U>
which will be completed with the result of the function combine
repeatedly applied to the success value for each future in self
and the accumulated value initialized with initial
.
That is, it transforms a SequenceOf<Future<T>>
into a Future<U>
whose result is the combined value of the success values of each future.
The combine
method will be called asynchronously in order with the futures in self
once it has been completed with success. Note that the future's underlying task will execute concurrently with each other and may complete in any order.
Examples for Combining Futures
Given a few asynchronous functions which return a future:
func task1() -> Future<Int> {...}
func task2(value: Int = 0) -> Future<Int> {...}
func task3(value: Int = 0) -> Future<Int> {...}
Combining Futures - Example 1a
Suppose we want to chain them in a manner that the subsequent task gets the result from the previous task as input. Finally, we want to print the result of the last task:
task1().flatMap { arg1 in
return task2(arg1).flatMap { arg2 in
return task3(arg2).map { arg3 in
print("Result: \(arg3)")
}
}
}
Combining Futures - Example 1b
When the first task finished successfully, execute the next task - and so force:
task1()
.flatMap { arg1 in
return task2(arg1)
}
.flatMap { arg2 in
return task3(arg2)
}
.map { arg3 in
print("Result: \(arg3)")
}
The task are independent on each other but they require that they will be called in order.
Combining Futures - Example 1c
This is example 1b, in a more concise form:
task1()
.flatMap(f: task2)
.flatMap(f: task3)
.map {
print("Result: \($0)")
}
Combining Futures - Example 2
Now, suppose we want to compute the values of task1, task2 and task3 concurrently and then pass all three computed values as arguments to task4:
func task4(arg1: Int, arg2: Int, arg3: Int) -> Future<Int> {...}
let f1 = task1()
let f2 = task2()
let f3 = task3()
f1.flatMap { arg1 in
return f2.flatMap { arg2 in
return f3.flatMap { arg3 in
return task4(arg1, arg2:arg2, arg3:arg3)
.map { value in
print("Result: \(value)")
}
}
}
}
Unfortunately, we cannot easily simplify the code like in the first example. We can improve it when we apply certain operators that work like syntactic sugar which make the code more understandable. Other languages have special constructs like do-notation or For-Comprehensions in order to make such constructs more comprehensible.
Specify an Execution Context where callbacks will execute
The continuations registered above will execute concurrently and we should not make any assumptions about the execution environment where the callbacks will be eventually executed. However, there's a way to explicitly specify this execution environment by means of an Execution Context with an additional parameter for all methods which register a continuation:
As an example, define a GCD based execution context which uses an underlying serial dispatch queue where closures will be submitted asynchronously on the specified queue with the given quality of service class:
let queue = dispatch_queue_create("sync queue",
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,
QOS_CLASS_USER_INITIATED, 0))
let ec = GCDAsyncExecutionContext(queue)
Then, pass this execution context to the parameter ec
:
future.onSuccess(ec: ec) { value in
// we are executing on the queue "sync queue"
let data = value.0
let response = value.1
...
}
When the future completes, it will now submit the given closure asynchronously to the dispatch queue.
If we now register more than one continuation with this execution context, all continuations will be submitted virtually at the same time when the future completes, but since the queue is serial, they will be serially executed in the order as they have been submitted.
Note that continuations will always execute on a certain Execution Context. If no execution context is explicitly specified a private one is implicitly given, which means we should not make any assumptions about where and when the callbacks execute.
An execution context can be created in various flavors and for many concrete underlying execution environments. See more chapter "Execution Context".
Cancelling a Continuation
Once a continuation has been registered, it can be "unregistered" by means of a Cancellation Token:
First create a Cancellation Request which we can send a "cancel" signal when required:
let cr = CancellationRequest()
Then obtain the cancellation token from the cancellation request, which the future can monitor to test whether there is a cancellation requested:
let ct = cr.token
This cancellation token will be passed as an additional parameter to any function which register a continuation. We can share the same token for multiple continuations or anywhere where a cancellation token is required:
future.onSuccess(ct: ct) { value in
...
}
future.onFailure(ct: ct) { error in
...
}
Internally, the future will register a "cancellation handler" with the cancellation token for each continuation will be registered with a cancellation token. The cancellation handler will be called when there is a cancellation requested. A cancellation handler simply "unregisters" the previously registered continuation. If this happens and if the continuation takes a Try<T>
or an ErrorType
as parameter, the continuation will also be called with a corresponding error, namely a CancellationError.Cancelled
error.
We may later request a cancellation with:
cr.cancel()
When a cancellation has been requested and the future is not yet completed, a continuation which takes a success value as parameter, e.g. a closure registered withonSuccess
, will be unregistered and subsequently deallocated.
On the other hand, a continuation which takes a Try
or an error value as parameter, e.g. continuations registered withonComplete
and onFailure
, will be first unregistered and then called with a corresponding argument, that is with an error set to CancellationError.Cancelled
. If the future is not yet completed, it won't be completed due to the cancellation request, though. That is, when the completion handler executes, the corresponding future may not yet be completed:
future.onFailure(ct: ct) { error in
if CancellationError.Cancelled.isEqual(error) {
// the continuation has been cancelled
}
}
CancellationRequest
and CancellationToken
build a powerful and flexible approach to implement a cancellation mechanism which can be leveraged in other domains and other libraries as well.
Wrap an asynchronous function with a completion handler into a function which returns a corresponding future
Traditionally, system libraries and third party libraries pass the result of an asynchronous function via a completion handler. Using futures as the means to pass the result is just another alternative. However, in order unleash the power of futures for these functions with a completion handler, we need to convert the function into a function which returns a future. This is entirely possible - and also quite simple.
Here, the Promise
enters the scene!
As an example, use an extension for NSURLSession
which performs a very basic GET request using a NSURLSessionDataTask
which can be cancelled by means of a cancellation token. Without focussing too much on a "industrial strength" implementation it aims to demonstrate how to use a promise - and also a cancellation token:
Get a future from an asynchronous function that returns a Future<T>
func get(
url: NSURL,
cancellationToken: CancellationTokenType = CancellationTokenNone())
-> Future<(NSData, NSHTTPURLResponse)>
{
// First, create a Promise with the appropriate type parameter:
let promise = Promise<(NSData, NSHTTPURLResponse)>()
// Define the session and its completion handler. If the request
// failed, we reject the promise with the given error - otherwise
// we fulfill it with a tuple of NSData and the response:
let dataTask = self.dataTaskWithURL(url) {
(data, response, error) -> Void in
if let e = error {
promise.reject(e)
}
else {
promise.fulfill(data!, response as! NSHTTPURLResponse)
// Note: "real" code would check the data for nil and
// response for the correct type.
}
}
// In case someone requested a cancellation, cancel the task:
cancellationToken.onCancel {
dataTask.cancel() // this will subsequently fail the task with
// a corresponding error, which will be used
// to reject the promise.
}
// start the task
dataTask.resume()
// Return the associated future from the new promise above. Note that
// the property `future` returns a weak Future<T>, so we need to
// explicitly unwrap it before we return it:
return promise.future!
}
Now we can use it as follows:
let cr = CancellationRequest()
session.get(url, cr.token)
.map { (data, response) in
guard 200 == response.statusCode else {
throw URLSessionError.InvalidStatusCode(code: response.statusCode)
}
guard response.MIMEType != nil &&
!response.MIMEType!.lowercaseString.hasPrefix("application/json") else {
throw URLSessionError.InvalidMIMEType(mimeType: response.MIMEType!)
}
...
let json = ...
return json
}
Installation
Carthage
Note: Carthage only supports dynamic frameworks which are supported in Mac OS X and iOS 8 and later.
- Follow the instruction Installing Carthage to install Carthage on your system.
- Follow the instructions Adding frameworks to an application. Then add
github "couchdeveloper/FutureLib"
to your Cartfile.
CocoaPods
As a minimum, add the following line to your Podfile:
pod 'FutureLib'
The above declaration loads the most recent version from the git repository.
You may specify a certain version or a certain range of available versions. For example:
pod 'FutureLib', '~> 1.0'
This automatically selects the most recent version in the repository in the range from 1.0.0 and up to 2.0, not including 2.0 and higher.
See more help here: Specifying pod versions.
Example Podfile:
# MyProject.Podfile
use_frameworks!
target 'MyTarget' do
pod 'FutureLib', '~> 1.0' # Version 1.0 and the versions up to 2.0, not including 2.0 and higher
end
After you edited the Podfile, open Terminal, cd to the directory where the Podfile is located and type the following command in the console:
$ pod install