RXPromise
An Objective-C Class which implements the Promises/A+ specification.
Install / Use
/learn @couchdeveloper/RXPromiseREADME
RXPromise
A thread safe implementation of the Promises/A+ specification in Objective-C with extensions.
If you like a more modern "Skala-like" futures and promise library implemented in Swift, you may look at FutureLib.
Important Note:
For breaking changes and API extensions please read the CHANGELOG document.
A Brief Feature List:
-
Employs the asynchronous "non-blocking" style
-
Supports chained continuations
-
Supports cancellation
-
Simplifies error handling through error propagation
-
Thread-safe implementation
-
Handlers can execute on diverse execution contexts, namely
dispatch_queue,NSThread,NSOperationQueueandNSManagedObjectContext. -
RXPromiseobjects are lightweight.
Credits
RXPromise has been inspired by the Promises/A+ specification which defines an open standard for robust and interoperable implementations of promises in JavaScript.
Much of the credits go to their work and to those smart people they based their work on!
How to Install
For install instructions, please refer to: INSTALL
Contents
- Introduction
- What is a Promise
- Where Can We Use Promises
- A Non-Trivial Example
- Understanding Promises
- The Resolver Aspect
- The Promise Aspect
- Using a Promise at the Resolver Site
- The Asynchronous Task's Responsibility
- Creating a Promise
- Resolving a Promise
- Forwarding Cancellation
- Using a Promise at the Call-Site
- Defining a Continuation
- A Continuation Returns a Promise
- Chaining
- Branching
- The then, thenOn and thenOnMain Property
- The Execution Context
- Error Propagation
- Cancellation
Introduction
What Is A Promise
In general, a promise represents the eventual result of an asynchronous task, respectively the error reason when the task fails. Equal and similar concepts are also called future, deferred or delay (see also wiki article: Futures and promises).
The RXPromise implementation strives to meet the requirements specified in the Promises/A+ specification as close as possible. The specification was originally written for the JavaScript language but the architecture and the design can be implemented in virtually any language.
Asynchronous non-blocking
A RXPromise employs the asynchronous non-blocking style. That is, a call-site can invoke an asynchronous task which immediately returns a RXPromise object. For example, starting an asynchronous network request:
RXPromise* usersPromise = [self fetchUsers];
The asynchronous method fetchUsers returns immediately and its eventual result will be represented by the returned object, a promise.
Given a promise, a call-site can obtain the result respectively the error reason and define how to continue with the program when the result is available through "registering" a Continuation.
Basically, a "Continuation" is a completion handler and an error handler, which are blocks providing the result respectively the error as a parameter. Having a promise, one or more continuations can be setup any time.
Registering a continuation will be realized with one of three properties of RXPromise: then, thenOn or thenOnMain, and providing the definitions of the handler blocks:
usersPromise.then(^id(id result){
NSLog(@"Users: %@", result);
return nil;
},
^id(NSError* error){
NSLog(@"Error: %@", error);
return nil;
});
A more thorough explanation of the "Continuation" is given in chapter Understanding Promises and Using a Promise at the Call-Site.
Thread-Safety
RXPromise's principal methods are all asynchronous and thread-safe. That means, a particular implementation utilizing RXPromise will resemble a purely asynchronous and also thread-safe system, where no thread is ever blocked. (There are a few exceptions where certain miscellaneous methods do block).
Explicit Execution Context
The Execution Context defines where the continuation (more precisely, the completion handler or the error handler) will finally execute on. When setting up a continuation for a RXPromise we can explicitly specify the execution context using the thenOn and the thenOnMain property. The execution context is used to ensure concurrency requirements for shared resources which will be accessed concurrently in handlers and from elsewhere. The execution context can be a dispatch queue, a NSOperationQueue, a NSThread or even a NSManagedObjectContext.
See also The Execution Context.
Cancellation
Additionally, RXPromise supports cancellation, which is invaluable in virtual every real application.
For more details about Cancellation please refer to chapter Cancellation.
Set of Helper Methods
The library also provides a couple of useful helper methods which makes it especially easy to manage a list or a group of asynchronous operations. Please refer to the source documentation.
Where Can We Use Promises?
Unquestionable, our coding style will become increasingly asynchronous. The Cocoa API already has a notable amount of asynchronous methods which provide completion handlers and also has numerous frameworks which support the asynchronous programming style through the delegate approach.
However, getting asynchronous problems right is hard, especially when the problems get more complex.
With Promises it becomes far more easy to solve asynchronous problems. It makes it straightforward to utilize frameworks and APIs that already employ the asynchronous style. A given implementation will look like it were synchronous, yet the solution remains completely asynchronous. The code also becomes concise and - thanks to Blocks - it greatly improves the locality of the whole asynchronous problem and thus the code becomes comprehensible and easy to follow for others as well.
A Non-trivial Example
Imagine, our objective is to implement the task described in the six steps below:
- Asynchronously perform a login for a web service.
- Then, if that succeeded, asynchronously fetch a list of objects as
JSON. - Then, if that succeeded, parse the
JSONresponse in a background thread. - Then, if that succeeded, create managed objects from the JSON and save them asynchronously to the persistent store, using a helper method
saveWithChildContext:(see below). - Then, if this succeeded, update the UI on the main thread.
- Catch any error from above steps.
Suppose, we have already implemented the following asynchronous methods:
In the View Controller:
/**
Performs login on a web service. This may ask for user credentials
in a separate UI.
If the operation succeeds, fulfills the returned promise with @"OK",
otherwise rejects it with the error reason (for example the user
cancelled login, or the authentication failed on the server).
*/
- (RXPromise*) login;
/**
Perform a network request to obtain a JSON which contains "Objects".
If the operation succeeds, fulfills the returned promise with a
`NSData` object containing the JSON, otherwise rejects it with the
error reason.
*/
- (RXPromise*) fetchObjects;
A real application would also use Core Data for managing a persistent store. Our "Core Data Stack" is quite standard having a "main context" executing on the main thread, whose parent is the "root context" running on a private queue with a backing store. Assuming this Core Data stack is already setup, we implement the following method:
/**
Saves the chain of managed object contexts starting with the child
context and ending with the root context which finally writes into
the persistent store.
If the operation succeeds, fulfills the returned promise with the
childContext object, otherwise rejects it with the error reason.
*/
- (RXPromise*) saveWithChildContext:(NSManagedObjectContext*)childContext;
Having those methods, the following single statement asynchronously executes the complex task defined in the six steps above:
RXPromise* fetchAndSaveObjects =
[self login]
.then(^id(id result){
return [self fetchObjects];
}, nil)
.then(^id(NSData* json){
NSError* error;
id jsonArray = [NSJSONSerialization JSONObjectWithData:json
options:0
error:&error];
if (jsonArray) {
NSAssert([jsonArray isKindOfClass:[NSArray class]]); // web service contract
return jsonArray; // parsing succeeded
}
else {
return error; // parsing failed
}
}, nil)
.then(^id(NSArray* objects){
// Parsing succeeded. Parameter objects is an array containing
// NSDictionaries representing a type "object".
// Create managed objects from the JSON and save them into
// Core Data:
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
moc
