pinterest/PINFuture

Name: PINFuture

Owner: Pinterest

Description: An Objective-C future implementation that aims to provide maximal type safety

Created: 2016-12-05 01:48:32.0

Updated: 2018-05-23 08:12:32.0

Pushed: 2018-05-22 22:05:41.0

Homepage:

Size: 625

Language: Objective-C

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

PINFuture

CI Status Version License Platform

Installation

PINFuture is available through CocoaPods. To install it, simply add the following line to your Podfile:

"PINFuture"
Overview

PINFuture is an Objective-C implementation of the asynchronous primitive called “Future”. This library differs from other Objective-C implementations of Future primarily because it aims to preserve type safety using Objective-C generics.

What is a Future?

A Future is a wrapper for “a value that will eventually be ready”.

A Future can have one of 3 states and usually begins in the “Pending” state. “Pending” means that the final value of the Future is not yet known but is currently being computed. The Future will eventually transition to either a “Fulfilled” state and contain a final value, or transition to a “Rejected” state and contain an error object. “Fulfilled” and “Rejected” are terminal states, and the value/error of a Future cannot change after the first fulfill or reject transition.

State diagram for a Future

Examples
Method signatures

Callback style

oid)logInWithUsername:(NSString *)username
             password:(NSString *)password
              success:( void (^)(User *user) )successBlock
              failure:( void (^)(NSError *error) )failureBlock;

Future style

INFuture<User *> *)logInWithUsername:(NSString *)username 
                            password:(NSString *)password;
Chain asynchronous operations

Callback style

f showSpinner];
r logInWithUsername:username password:password success:^(User *user) {
[Posts fetchPostsForUser:user success:^(Posts *posts) {
    dispatch_async(dispatch_get_main_queue(), ^{
       [self hideSpinner];
        // update the UI to show posts
    });
} failure:^(NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self hideSpinner];
        // update the UI to show the error
    });
}];
ilure:^(NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
    [self hideSpinner];
    // update the UI to show the error
});

Future style

f showSpinner];
uture<User *> *userFuture = [User logInWithUsername:username password:password];
uture<Posts *> *postsFuture = [PINFutureMap<User *, Posts *> flatMap:userFuture executor:[PINExecutor main] transform:^PINFuture<Posts *> *(User *user) {
return [Posts fetchPostsForUser:user];

tsFuture executor:[PINExecutor main] completion:^{
[self hideSpinner];

tsFuture executor:[PINExecutor main] success:^(Posts *posts) {
// update the UI to show posts
ilure:^(NSError *error) {
// update the UI to show the error

Stubbing an async function in a test

Callback style

tub([fileMock readContentsPath:@"foo.txt" 
                       success:OCMOCK_ANY
                       failure:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
(void)(^successBlock)(NSString *) = nil;
[invocation getArgument:&successBlock atIndex:3];
if (successBlock) {
    successBlock(@"fake contents");
}

Future style

tub([fileMock readContentsPath:@"foo.txt"]).andReturn([PINFuture<NSString *> withValue:@"fake contents"]);
Handling values

To access the final value of a Future, register success and failure callbacks. If you only want to know when a Future completes (and not the specific value or error), register a complete callback.

Callbacks will be dispatched in the order that they are registered. However, depending on your specified executor, the blocks might execute in a different order or even execute concurrently.

Threading model

Whenever you pass a callback block you must also pass a required executor: parameter. The executor determines where and when your block will be executed.

Common values for executor:

A good rule of thumb: Use [PINExecutor background] if work that your callback block does is thread-safe and if the work doesn't need to be executed from the Main thread (e.g. because it's touching UIKit).

Preserving type safety

PINFuture makes use of Objective-C generics to maintain the same type safety that you'd have with callbacks.

Future<NSNumber *> withValue:@"foo"]; // Compile error.  Good!

In Objective-C, type parameters are optional. It's a good practice to always specify them for a PINFuture.

Future withValue:@"foo"]; // This compiles but will likely blow up with "unrecognized selector" when the value is used.
Blocking on a result

PINFuture is non-blocking and provides no mechanism for blocking. Blocking a thread on the computation of an async value is generally not a good practice, but is possible using Grand Central Dispatch Semaphores

Handling exceptions

PINFuture does not capture Exceptions thrown by callbacks. On platforms that PINFuture targets, NSExceptions are generally fatal. PINFuture deals with NSErrors.

API Reference
Constructing
withValue:

Construct an already-fulfilled Future with a value.

uture<NSString *> stringFuture = [PINFuture<NSString *> withValue:@"foo"];
withError:

Construct an already-rejected Future with an error.

uture<NSString *> stringFuture = [PINFuture<NSString *> withError:[NSError errorWithDescription:...]];
withBlock:

Construct a Future and fulfill or reject it by calling one of two callbacks. This method is generally not safe since because there's no enforcement that your block will call either resolve or reject. This is most useful for writing a Future-based wrapper for a Callback-based method. You'll find this method used extensively in the PINFuture wrappers of Cocoa APIs.

uture<NSString *> stringFuture = [PINFuture<NSString *> withBlock:^(void (^ fulfill)(NSString *), void (^ reject)(NSError *)) {
[foo somethingAsyncWithSuccess:resolve failure:reject];

executor:block:

Construct a Future by executing a block that returns a Future. The most common use case for this is to dispatch some chunk of compute-intensive work off of the the current thread. You should prefer this method to withBlock: whenever you can return a Future because the compiler can enforce that all code paths of your block will return a Future.

uture<NSNumber *> fibonacciResultFuture = [PINFuture<NSNumber *> executor:[PINExecutor background] block:^PINFuture *() {
NSInteger *fibonacciResult = [self computeFibonacci:1000000];
return [PINFuture<NSNumber *> withValue:fibonacciResult];

Transformations

In order to achieve type safety for an operation like map that converts from one type of value to another type, we have to jump through some hoops because of Objective-C's rudimentary support for generics. map and flatMap are class methods on the class PINFutureMap. The PINFutureMap class has two type parameters: FromType and ToType.

Error handling with transformations map
uture<NSString *> stringFuture = [PINFutureMap<NSNumber *, NSString *> map:numberFuture executor:[PINExecutor background] transform:^NSString *(NSNumber * number) {
return [number stringValue];

flatMap
uture<UIImage *> imageFuture = [PINFutureMap<User *, UIImage *> flatMap:userFuture executor:[PINExecutor background] transform:^PINFuture<NSString *> *(User *user) {
return [NetworkImageManager fetchImageWithURL:user.profileURL];

mapError
uture<NSString *> *stringFuture = [File readUTF8ContentsPath:@"foo.txt" encoding:EncodingUTF8];
ngFuture = [fileAFuture executor:[PINExecutor immediate] mapError:^NSString * (NSError *errror) {
return "";  // If there's any problem reading the file, continue processing as if the file was empty.

flatMapError
uture<NSString *> *stringFuture = [File readUTF8ContentsPath:@"tryFirst.txt"];
ngFuture = [fileAFuture executor:[PINExecutor background] flatMapError:^PINFuture<NSString *> * (NSError *errror) {
if ([error isKindOf:[NSURLErrorFileDoesNotExist class]) {
    return [File readUTF8ContentsPath:@"trySecond.txt"];
} else {
    return [PINFuture withError:error];  // Pass through any other type of error
}

Gathering
gatherAll
ray<NSString *> fileNames = @[@"a.txt", @"b.txt", @"c.txt"];
ray<PINFuture<NSString *> *> *fileContentFutures = [fileNames map:^ PINFuture<NSString *> *(NSString *fileName) {
return [File readUTF8ContentsPath:fileName];

uture<NSArray<NSString *> *> *fileContentsFuture = [PINFuture<NSString *> gatherAll:fileContentFutures];
eContentsFuture executor:[PINExecutor main] success:^(NSArray<NSString *> *fileContents) {
// All succceeded.
ilure:^(NSError *error) {
// One or more failed.  `error` is the first one to fail.

gatherSome

Experimental. This API may change to improve type safety.

ray<NSString *> fileNames = @[@"a.txt", @"b.txt", @"c.txt"];
ray<PINFuture<NSString *> *> *fileContentFutures = [fileNames map:^ PINFuture<NSString *> *(NSString *fileName) {
return [File readUTF8ContentsPath:fileName];

uture<NSArray *> *fileContentsOrNullFuture = [PINFuture<NSString *> gatherSome:fileContentFutures];
eContentsFuture executor:[PINExecutor main] success:^(NSArray *fileContents) {
// fileContents is an array of either `NSString *` or `[NSNull null]` depending on whether the source future resolved or rejected.
ilure:^(NSError *error) {
// This can't be reached.  If any of the source futures fails, there will be a `[NSNull null]` entry in the array.

Chaining side-effects (necessary evil)
chainSuccess:failure:

This is similar to success:failure except that a new Future is returned that does not fulfill or reject until the side-effect has been executed. This should be used sparingly. It should be rare that you want to have a side-effect, and even rarer to wait on a side-effect.

etch a user, and return a Future that resolves only after all NotificationCenter observers have been notified.
uture<User *> *userFuture = [self userForUsername:username];
Future = [userFuture executor:[PINExecutor main] chainSuccess:^(User *user) {
[[NSNotifcationCenter sharedCenter] postNotification:kUserUpdated object:user];
ilure:nil;
rn userFuture;
Convenience methods (experimental)
executeOnMain/executeOnBackground

We've observed that application code will almost always call with either executor:[PINExecutor main] or executor:[PINExecutor background]. For every method that takes an executor: there are 2 variations of that method, executeOnMain and executeOnBackground, that are slightly more concise (shorter by 22 characters).

The following pairs of calls are equivalent. The second call in each pair demonstrated the convenience method.

rFuture executor:[PINExecutor main] success:success failure:failure];
rFuture executeOnMainSuccess:success failure:failure];

rFuture executor:[PINExecutor background] success:success failure:failure];
rFuture executeOnBackgroundSuccess:success failure:failure];

uture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executor:[PINExecutor main] transform:transform];
uture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executeOnMainTransform:transform];

uture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executor:[PINExecutor background] transform:transform];
uture<Post *> *postFuture = [PINFutureMap<User, Post> map:userFuture executeOnBackgroundTransform:transform];
Roadmap
“Future” versus “Callback”
“Future” versus “Task”
Alternatives
Swift
Scala
Objective-C
Java
C++
JavaScript
Other inspiration
Design decisions

These decisions are possibly controversial but deliberate.

Authors
License

Copyright 2016-2018 Pinterest, Inc

Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.