A promise implementation for objective-c: CKPromise

Now that we went through all promise technical details in the past articles: Promises and ObjectiveC: no more callback hell, Promises: basics, and Promises: advanced, it’s time to discuss  about one of the available implementations, and for subjective reasons I chose CKPromise. Other good implementations that I know of are PromiseKit and RXPromise.

CKPromise focuses only on implementing the Promise/A+ specs. Nothing else. All callbacks are currently scheduled on the main thread, for simplicity reasons. If there are requests to add support for scheduling callbacks on user-defined queues, I might take that into consideration, and add the support. Thus, I’m trying to follow the YAGNI model.

I think the best way to explain something is via examples, so I’ll try to cover all the features provided by promises, features discussed in the previous articles.

1. Basic usage: CKPromise can be resolved or rejected via the methods with the same name. Callbacks can be added via the then() method, which returns a block, thus it will be called in the C-style rather than the objective-c style.

- (CKPromise*)loginWithEmail:(NSString*)email password:(NSString*)password {
    NSDictionary *params = @{@"email":email, @"password":password};
    CKPromise *promise = [[CKPromise alloc] init];
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    [manager POST:@"http://myserver.com/api/login"
       parameters:params
          success:^(AFHTTPRequestOperation *operation, id responseObject) {
              MYUser *user = [[MYUser alloc] initWithDictionary: responseObject];
              [promise resolve:user];
          }
          failure:^(AFHTTPRequestOperation *operation, NSError *error) {
              [promise reject:error];
          }];
}

- (IBAction)login:(id)sender {
    [apiClient loginWithEmail:emailTextField.text
                     password:passwordTextField.text].then(^(MYUser *user){
        [self showProgress:NO];
        // move to the regular user or admin screen, based on the user rights
    }, ^(NSError *error){
        [self showProgress:NO];
        // display an alert view
    });

    [self showProgress:YES];
}

CKPromise exposes some convenience methods that allow a simple usage in case we are interested only about the success or failure of the promise, or we just want a piece of code to be executed regardless the resolution of the promise: success, failure, always. Their usage is very similar to the one of then().

CKPromise also exposes resolveWith: and rejectWith: that allow you to resolve/reject the promise with multiple values. For example in the case of the login promise, one could have also want to also provide the raw object. This is how the promise would have been used:

[promise resolveWith:user, responseObject, nil];

// later in the code...
[promise success:^(MYUser *user, NSDictionary *serverData) {
    // the desired logic
}];

 

2. Sync-ish coding style when dealing with multiple async operations that need to be executed in cascade. This has been exemplified in the first article of the promises series, I’ll place it here too.

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *bookData = @{@"name": @"Harry Potter"};
[manager POST:@"http://myserver.com/validator/book"
    parameters:bookData].promise.done(^{
    return [manager POST:@"http://myserver.com/book"
              parameters:bookData].promise;

}).success(^(NSString *bookId){
    return [manager GET:[@"http://myserver.com/book/" stringByAppendingString:bookId]
             parameters:nil].promise;

}).success(^(NSDictionary *bookDetails){
    // inform the user that the book was added, move to the appropriate screen

}).failure(^(NSError *error) {
    if([error.domain isEqual:NSURLErrorDomain]) {
        // inform the user that there was a server communication problem
    } else {
        // inform the user about the problem
        // error.domain/error.code can be used to identify which one of the
        // three operations failed
    }
});

The code sample assumes that AFHTTPRequestOperation has a category that exposes a promise method/property. I leave the implementation of this method as an exercise for the reader. Hint: it’s not much different that the login method discussed earlier in the article.

Chains like this work no matter how many async operations we need to do. The chain of async operations will continue as long as the callbacks return a promise that corresponds to an async operation. But you’re not restricted to return only promises in a promise chain, this brings us to the next best thing provided by promises.

 

3. Data transformation: a chain of promises can be used to gradually transform a piece of raw data into a finite form. An example can be a distributed architecture where its components communicate via serialised objects, and that uses cryptography when sending data.

[communicationStream readDataAsync].success(^(NSData *encryptedData, NSUInteger senderTimestamp) {
    // throws MYDecriptException if decription fails
    return [data decryptAsyncWithTimestamp:senderTimestamp];
}).success(^(NSData *decriptedData) {
    // throws MYDataParseException if decripted data could not be parsed
    return [NSDictionary asyncParseMessageData];
}).success(^(NSDictionary *messageDict) {
    // throws MYInvalidMessage if the message dict doesn't contain the proper fields 
    return [[MYMessage alloc] initWithDictionary:messageDict];
}).failure(^(NSException *ex) {
    //based on the exception type we know which operation failed
});

We can see how natural we can transform pieces of data from a raw form to a finite one. Also if we take a look of how the sync version would look like, we notice that we have a very similar code flow.

@try {
    NSUInteger timestamp = 0;
    NSData *cryptedData = [communicationStream readData:&timestamp];
    NSData *decryptedData = [cryptedData decryptWithTimestamp:timestamp];
    NSDictionary *messageDict = [NSDictionary dictionaryWithMessageData:decryptedData];
    MYMessage = [[MYMessage alloc] initWithDictionary:messageDict];
} @catch(NSException *ex) {
    // either based on the exception type we know which operation failed
    // or we add multiple exception handlers
};

 

4. Chain derailing: at a certain point in a chain a promise can decide to break the chain if it’s not satisfied with the results, or can recover and continue the chain with another values.

Chain recovering example:

[authenticator loginWithGoogle].failure(^{
    // not slightly interested about the error, thus the callback has no params
    return [authenticator loginWithFacebook];
}).failure(^{
    return [authenticator loginWithTwitter];
}).then(^{
   // I managed to authenticate with the server
}, ^{
   // none of the allowed authentication methods succeeded
   // I'm not authenticated
});

As for the chain breaking, the cryptic message communication flow described earlier in article serves as a good example. Basically at any time during the data processing the corresponding promise (e.g. the decryption or the data parsing one) can throw an exception or return a failed promise that has the virtual effect of moving straight to the failure callback. I call it virtual because all remaining promises is the chain receive the failure, however as none of them is interested about the failure, this is passed along until the failure callback is reached.

The powerfulness of promises come from the fact that not only they allow clients to schedule as many callbacks as need, but they also react to the values returned by the callbacks, allowing the clients to instruct how the promise should behave next (although the clients in fact instruct another promise). If I haven’s said it before, promises are awesome! This being said, I hope you enjoyed the last couple of articles of the subject of promises and I encourage you to use them as much as possible wherever you need support for asynchronous execution. You won’t regret it!

P.S. As usual, if you have any questions/comments you can post them as comments below.

Leave a comment

Your email address will not be published. Required fields are marked *