PromiseKitのthen,catch,finally,whenについて

参考のように僕の扱うプロジェクトでもコールバックの階層が深い部分が悩みの一つでした. これが一概に悪いとも言えないのですが, 非同期処理が追加されるにつれ階層が深くなるのはやはり良くありません.
そこで今回, 非同期処理をスッキリ書ける[PromiseKit](https://github.com/mxcl/PromiseKit)を導入しようかと思い, 導入前に挙動確認をしておきます.

**参考**
[コールバック地獄からの脱却！複雑なiOSアニメーションをPromiseの決定版ライブラリPromiseKitですっきり実装する！！！](http://qiita.com/appwatcher/items/7ed4c9da65ad0b53d417)

PromiseKitの導入方法は[公式サイト](https://github.com/mxcl/PromiseKit)を参照してください. ここでは検証コードだけを記述していきます.

ソースコード

```objc:ViewController.m
#import "ViewController.h"
#import <PromiseKit/PromiseKit.h>

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // ここに検証コードを書く //

}

// 指定秒後にfulfillされるpromiseを返す
- (PMKPromise *) promiseFulfillWithDelay:(double)delay name:(NSString *)name {
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@ running...", name);
            fulfill(name);
        });
    }];
}

// 指定秒後にrejectされるpromiseを返す
- (PMKPromise *) promiseRejectWithDelay:(double)delay name:(NSString *)name {
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@ running...", name);
            NSError *e = [NSError errorWithDomain:name code:-1 userInfo:nil];
            reject(e);
        });
    }];
}

- (NSTimeInterval) currentTimeSec {
    return [[NSDate date] timeIntervalSince1970];
}
@end
```

#基本
##then

```objc
    PMKPromise *pA = [self promiseFulfillWithDelay:2.0 name:@"A"];

    pA.then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    A running...
    then A
    finally

fulfillされるとthenが実行されます.

##catch

```objc
    PMKPromise *pB = [self promiseRejectWithDelay:1.0 name:@"B"];

    pB.then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    B running...
    catch B
    finally

rejectされるとcatchが実行されます.

##finally

promiseはfulfillかrejectかのどちらか一方の状態にしかなりません. ただし, fulfillでもrejectでもfinallyは実行されます.

#組み合わせる
##例1

```objc
    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseFulfillWithDelay:1.0 name:@"B"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseRejectWithDelay:1.0 name:@"C"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    A running...
    then A
    B running...
    then B
    C running...
    catch C
    finally

thenブロックの中でpromiseオブジェクトを`return`すると後続するthen/catchにつなげることができます.

##例2

```objc
    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseRejectWithDelay:1.0 name:@"B"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseFulfillWithDelay:1.0 name:@"C"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    A running...
    then A
    B running...
    catch B
    finally

メソッドチェーンの途中でrejectされると後続のthenをスキップしてcatchが呼ばれます.
上記例の場合, Cは実行されずにcatchでBのNSErrorを受け取り, 最後にfinallyが呼ばれます.

##例3

```objc
    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseRejectWithDelay:1.0 name:@"B"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseFulfillWithDelay:1.0 name:@"C"];
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
        return [self promiseFulfillWithDelay:0.5 name:@"D"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    A running...
    then A
    B running...
    catch B
    D running...
    then D
    finally

上記例ではBがrejectされcatchが呼ばれているますが, そのブロックの中でpromiseオブジェクトを`return`しています. そして, 後続するthen/catchへとつながっています.

つまりthenでもcatchでもブロック中でpromiseオブジェクトを`return`することで処理を続けていくことができます.

##例4

```objc
    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return @{ @"hoge": @"foo" };
    }).then(^(id obj) {
        NSLog(@"then %@", obj);
        // no return
    }).then(^(id obj) {
        NSLog(@"then %@", obj);
    });
```

結果

    A running...
    then A
    then { hoge = foo; }
    then (null)

実はpromiseオブジェクト以外のオブジェクトでも`return`できます. その場合はthenが呼ばれ引数にそのオブジェクトを渡します.
更に`return`をしなくてもthenがつながっていれば, そのthenが呼ばれます. その場合, 引数の値はnilになります.

#when

whenは複数の非同期処理を並行処理しすべてfulfillされた時, thenが呼ばれます.

```objc
    PMKPromise *pA = [self promiseFulfillWithDelay:3.0 name:@"A"];
    PMKPromise *pB = [self promiseFulfillWithDelay:1.0 name:@"B"];
    PMKPromise *pC = [self promiseFulfillWithDelay:2.0 name:@"C"];
    NSTimeInterval startT = [self currentTimeSec];

    [PMKPromise when:@[ pA, pB, pC ]].then(^(NSArray *results) {
        NSTimeInterval t = [self currentTimeSec] - startT;
        NSLog(@"then %f %@", t, results);
    }).catch(^{
        NSLog(@"catch");
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    B running...
    C running...
    A running...
    then 3.298405 ( A, B, C )
    finally

thenは最後の非同期処理がfulfillされた時点で呼ばれるので上記例ではだいたい3秒後になります.
結果はNSArrayで渡されますが, 要素の順番は非同期処理が完了した順ではなく, whenに指定した配列の順番にならいます.
また, NSArrayではなくNSDictionaryで指定することもできます.

```objc
    PMKPromise *pA = [self promiseFulfillWithDelay:3.0 name:@"A"];
    PMKPromise *pB = [self promiseFulfillWithDelay:1.0 name:@"B"];
    PMKPromise *pC = [self promiseFulfillWithDelay:2.0 name:@"C"];
    NSTimeInterval startT = [self currentTimeSec];

    [PMKPromise when:@{ @"a": pA, @"b": pB, @"c": pC }].then(^(NSDictionary *results) {
        NSTimeInterval t = [self currentTimeSec] - startT;
        NSLog(@"then %f %@", t, results);
    }).catch(^{
        NSLog(@"catch");
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    B running...
    C running...
    A running...
    then 3.001315 { a = A; b = B; c = C; }
    finally

whenはひとつでもrejectされた場合, 即catchが呼ばれます.

```objc
    PMKPromise *pA = [self promiseFulfillWithDelay:3.0 name:@"A"];
    PMKPromise *pB = [self promiseFulfillWithDelay:1.0 name:@"B"];
    PMKPromise *pC = [self promiseRejectWithDelay:2.0 name:@"C"];

    NSTimeInterval startT = [self currentTimeSec];
    [PMKPromise when:@[ pA, pB, pC ]].then(^(NSArray *results) {
        NSLog(@"then %@", results);
    }).catch(^{
        NSTimeInterval t = [self currentTimeSec] - startT;
        NSLog(@"catch %f", t);
    }).finally(^{
        NSLog(@"finally");
    });
```

結果

    B running...
    C running...
    catch 2.199885
    finally
    A running...

上記例ではCがrejectされたので即catchが呼ばれています.
注意したいのはfinallyの後にAが走っていることです. rejectされた場合でも非同期処理自体をキャンセルはしてくれないようです.

なお, allというメソッドもありますが, allはwhenのエイリアスなようです. 挙動はwhenと同じでした.

#finally -> then?

ちなみにfinallyの後にthenをつけることもできました. ただし, finallyブロックでは`return`することができません(ビルドエラーになります).
こんな使い方しませんが...

```objc
    [self promiseFulfillWithDelay:1.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).finally(^{
        NSLog(@"finally 1");
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).finally(^{
        NSLog(@"finally 2");
    });
```

結果

    A running...
    then A
    finally 1
    then (null)
    finally 2