Skip to content

FutureLib is a pure Swift 2 library implementing Futures & Promises inspired by Scala.

License

Notifications You must be signed in to change notification settings

couchdeveloper/FutureLib

Repository files navigation

FutureLib

Build Status GitHub license Swift 2.2 Platforms MacOS | iOS | tvOS | watchOS Carthage Compatible CocoaPods

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

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 type T or a value conforming to the Swift protocol ErrorType.

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 Us 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

Note: Carthage only supports dynamic frameworks which are supported in Mac OS X and iOS 8 and later.

  1. Follow the instruction Installing Carthage to install Carthage on your system.
  2. Follow the instructions Adding frameworks to an application. Then add
    github "couchdeveloper/FutureLib"
    to your Cartfile.

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

About

FutureLib is a pure Swift 2 library implementing Futures & Promises inspired by Scala.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages