Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

NotificationsYou must be signed in to change notification settings

couchdeveloper/FutureLib

Repository files navigation

Build StatusGitHub licenseSwift 2.2Platforms MacOS | iOS | tvOS | watchOSCarthage CompatibleCocoaPods

FutureLib is a pure Swift 2 library implementing Futures & Promises inspired byScala,Promises/A+ and a cancellation concept withCancellationRequest andCancellationToken similar toCancellation 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 theeventual result of anasynchronous function. Say, the computed value is of typeT, the asynchronous functionimmediately returns a value of typeFuture<T>:

func doSomethingAsync()->Future<Int>

When the function returns, the returned future is not yetcompleted - but there executes a background task which computes the value andeventually completes the future. We can say, the returned future is aplaceholder for the result of the asynchronous function.

The underlying task may fail. In this case the future will be completed with anerror. 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 witheither thevalue or anerror.

In order to represent that kind of result, a future uses an enum typeTry<T> internally.Try is a kind of variant, ordiscriminated union which containseither a valueor an error. Note, that there are other Swift libraries with a similar type which is usually namedTry. The nameTry 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 likedoSomethingAsync() above. In order to retrieve the result, we register acontinuation 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 ofread 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 aPromise:

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 oneassociated future. A promise can beresolved with either the computed value or an error. Resolving a promise with aTry 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:

publicfunc doSomethingAsync-> Future<Int>{// First, create a promise:letpromise=Promise<Int>()    // Start the asynchronous work:doSomethingAsyncWithCompletion{(data, error)->Voidiniflet 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? Andwhen should we attempt to retrieve it?

Well, it should be clear, that we can obtain the value onlyafter the future has been completed witheither the computed valueor 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

Methodvalue 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

varresult:Try<ValueType>?

If the future is completed returns its result, otherwise it returnsnil. 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 useContinuations:

Non-Blocking Access with Continuations

In order to retrieve the result from a future in a non-blocking manner we can use aContinuation. A continuation is a closure which will beregistered 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 withcombinators which differ in their signature. Most continuations have a parameterresult asTry<T>,value asT orerror asErrorType 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 isonComplete:

onComplete

func onComplete<U>(f:Try<T>->U)

MethodonComplete registers a continuation which will be called when the future has been completed. It gets passed theresult as aTry<T> of the future as its argument:

future.onComplete{ resultin    // result is of type Try<T>}

whereresult is of typeTry<T> whereT is the type of the computed value of the functiondoSomethingAsync.result may contain either a value of typeT or an error, conforming to protocolErrorType.

Almost all methods which register a continuation are implemented in terms ofonComplete.

There a few approaches to get the actual value of a result:

letresult:Try<Int>=...switch result{case.Success(let value):print("Value:\(value)")case.Failure(let error):print("Error:\(error)")}

The next basic methods areonSuccess andonFailure, which get called when the future completes with success respectively with an error.

onSuccess

func onSuccess<U>(f:T->throws U)

With methodonSuccess we register a continuation which gets called when the future has been completed with success:

future.onSuccess{ valuein// value is of type T}

onFailure

func onFailure<U>(f:T->U)

WithonFailure we register a continuation which gets called when the future has been completed with an error:

future.onFailure{ errorin// error conforms to protocol `ErrorType`}

Combinators

Continuations will also be registered withCombinators. A combinator is a method which returns a new future. There are quite a few combinators, most notablemap andflatMap. 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:Tthrows->U)->Future<U>

Methodmap returns a new future which is completed with the result of the functionf which is applied to the success value ofself. Ifself has been completed with an error, or if the functionf throws and error, the returned future will be completed with the same error. The continuation will not be called whenself fails.

Since the return type of combinators likemap is again a future we can combine them in various ways. For example:

fetchUserAsync(url).map{ userinprint("User:\(user)")return user.name()}.map{ nameinprint("Name:\(name)")}.onError{ errorinprint("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 isVoid sinceonError returnsVoid.

flatMap

func flatMap<U>(f:Tthrows->Future<U>)->Future<U>

MethodflatMap returns a new future which is completed with theeventual result of the functionf which is applied to the success value ofself. Ifself has been completed with an error the returned future will be completed with the same error. The continuation will not be called whenself fails.

An example:

fetchUserAsync(url).flatMap{ userinreturnfetchImageAsync(user.imageUrl)}.map{ imageindispatch_async(dispatch_get_main_queue()){self.image= image}}.onError{ errorinprint("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:ErrorTypethrows->T)->Future<T>`

Returns a new future which will be completed withself's success value or with the return value of the mapping functionf whenself fails.

recoverWith

func recoverWith(f:ErrorTypethrows->Future<T>)->Future<T>

Returns a new future which will be completed withself's success value or with the deferred result of the mapping functionf whenself fails.

Usually,recover orrecoverWith 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:

letfuture=computeString().recover{ errorinNSLog("Error:\(error)")return""}

filter

func filter(predicate:Tthrows->Bool)->Future<T>

Methodfilter returns a new future which is completed with the success value ofself if the functionpredicate applied to the value returnstrue. Otherwise, the returned future will be completed with the errorFutureError.NoSuchElement. Ifself 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{ strin}

transform

func transform<U>(s:Tthrows->U, f:ErrorType->ErrorType)->Future<U>

Returns a new Future which is completed with the result of functions applied to the successful result ofself or with the result of functionf applied to the error value ofself. Ifs 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 ofself. 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 ofself andother. Ifself 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 newFuture which will be completed with the result of the first completed future inself.

traverse

func traverse<U>(task:Tthrows->Future<U>)->Future<[U]>

For any sequence ofT, the asynchronous methodtraverse applies the functiontask to each value of the sequence (thus, getting a sequence of tasks) and then completes the returned future with an array ofUs once all tasks have been completed successfully.

letids=[14,34,28]ids.traverse{ idinreturnfetchUser(id)}.onSuccess{ usersin    // user is of type [User]}

The tasks will be executed concurrently, unless anexecution 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 futuresFuture<T> the methodsequence returns a new futureFuture<[T]> which is completed with an array ofT, where each element in the array is the success value of the corresponding future inself in the same order.

[fetchUser(14),fetchUser(34),fetchUser(28)].sequence{ usersin    // user is of type [User]}

results

func results()->Future<Try<T>>

For a sequence of futuresFuture<T>, the methodresult returns a new future which is completed with an array ofTry<T>, where each element in the array corresponds to the result of the future inself in the same order.

[fetchUser(14),fetchUser(34),fetchUser(28)].results{ resultsin    // results is of type [Try<User>]}

fold

func fold<U>(initial:U, combine T throws-> U)->Future<U>

For a sequence of futuresFuture<T> returns a new futureFuture<U> which will be completed with the result of the functioncombine repeatedly applied to the success value for each future inself and the accumulated value initialized withinitial.

That is, it transforms aSequenceOf<Future<T>> into aFuture<U> whose result is the combined value of the success values of each future.

Thecombine method will be called asynchronously in order with the futures inself 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{ arg1inreturntask2(arg1).flatMap{ arg2inreturntask3(arg2).map{ arg3inprint("Result:\(arg3)")}}}
Combining Futures - Example 1b

When the first task finished successfully, execute the next task - and so force:

task1().flatMap{ arg1inreturntask2(arg1)}.flatMap{ arg2inreturntask3(arg2)}.map{ arg3inprint("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>{...}letf1=task1()letf2=task2()letf3=task3()f1.flatMap{ arg1inreturn f2.flatMap{ arg2inreturn f3.flatMap{ arg3inreturntask4(arg1, arg2:arg2, arg3:arg3).map{ valueinprint("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 likedo-notation orFor-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 anExecution 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:

letqueue=dispatch_queue_create("sync queue",dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,QOS_CLASS_USER_INITIATED,0))letec=GCDAsyncExecutionContext(queue)

Then, pass this execution context to the parameterec:

future.onSuccess(ec: ec){ valuein// we are executing on the queue "sync queue"letdata= value.0letresponse= 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 willalways execute on a certainExecution Context. If no execution context is explicitly specified aprivate 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 aCancellation Token:

First create aCancellation Request which we can send a "cancel" signal when required:

letcr=CancellationRequest()

Then obtain the cancellation token from the cancellation request, which the future can monitor to test whether there is a cancellation requested:

letct= 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){ valuein...}future.onFailure(ct: ct){ errorin...}

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 aTry<T> or anErrorType as parameter, the continuation will also be called with a corresponding error, namely aCancellationError.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 aTry or an error value as parameter, e.g. continuations registered withonComplete andonFailure, will be first unregistered and then called with a corresponding argument, that is with an error set toCancellationError.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){ errorinifCancellationError.Cancelled.isEqual(error){// the continuation has been cancelled}}

CancellationRequest andCancellationToken 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, thePromise enters the scene!

As an example, use an extension forNSURLSession which performs a very basic GET request using aNSURLSessionDataTask 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 aFuture<T>

func get(    url:NSURL,    cancellationToken:CancellationTokenType=CancellationTokenNone())->Future<(NSData,NSHTTPURLResponse)>{// First, create a Promise with the appropriate type parameter:letpromise=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:letdataTask=self.dataTaskWithURL(url){(data, response, error)->Voidiniflet e= error{            promise.reject(e)}else{            promise.fulfill(data!, responseas!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:

letcr=CancellationRequest()session.get(url, cr.token).map{(data, response)inguard200== response.statusCodeelse{throwURLSessionError.InvalidStatusCode(code: response.statusCode)}guard response.MIMEType!=nil &&!response.MIMEType!.lowercaseString.hasPrefix("application/json")else{throwURLSessionError.InvalidMIMEType(mimeType: response.MIMEType!)}...letjson=...return json}

Installation

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

  1. Follow the instructionInstalling Carthage to install Carthage on your system.
  2. Follow the instructionsAdding frameworks to an application. Then add
    github "couchdeveloper/FutureLib"
    to your Cartfile.

As a minimum, add the following line to yourPodfile:

pod'FutureLib'

The above declaration loads themost recent version from the git repository.

You may specify a certain version or a certainrange 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.Podfileuse_frameworks!target'MyTarget'dopod'FutureLib','~> 1.0'# Version 1.0 and the versions up to 2.0, not including 2.0 and higherend

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


[8]ページ先頭

©2009-2025 Movatter.jp