2013/12/20

ReactiveCocoa 3.0に採用予定のSignalGeneratorについて

ReactiveCocoa 3.0に採用予定のSignalGeneratorは何らかの値からシグナルを生成するためのロジックを抽象化したものです。

本記事では、まず簡単にSignalGeneratorを紹介した後に、実際にSignalGeneratorを利用している例として、ReactiveCocoa 3.0開発版のrepeatretry:を見ていきます。

SignalGenerator概要

RACSignalGeneratorはSignalGeneratorの抽象基底クラスです。 通常はこのクラスのサブクラスであるRACDynamicSignalGeneratorなどを利用します。

RACSignalGeneratorでシグナルを生成するときには次のメソッドを利用します。RACSignalGeneratorでこれを呼ぶと必ずnilを返します。

- (RACSignal *)signalWithValue:(id)input;

クラスをわざわざ作る必要があるのか疑問に思われる方もいるかと重いますが、SignalGeneratorを使うのは次の2つの利点があります。

  1. SignalGeneratorはロジックをカプセル化しているので、これらを組み合わせて別のSignalGeneratorを作る、なんてことも容易にできるようになります。実際に、RACSignalGeneratorにはpostcompose:というメソッドを用意しています。
  2. SignalGeneratorはシグナル生成地に自分自身を参照できます。これによって再帰的なものを書くことができます。

RACDynamicSignalGenerator

RACDynamicSignalGeneratorRACSignalGeneratorのサブクラスです。このクラスのクラスメソッドgeneratorWithReflexiveBlock:を使うと、再帰的な処理を書くことができます。

これは次のようなメソッドです。シグナルを生成しようとするときに自分自身を参照することができるblockを指定します。

+ (instancetype)generatorWithReflexiveBlock:(RACSignal * (^)(id input, RACDynamicSignalGenerator *generator))block {
    RACDynamicSignalGenerator *generator = [self alloc];
    return [generator initWithBlock:^(id input) {
        return block(input, generator);
    }];
}

signalWithValue:は次のようにシンプルなものです。

- (RACSignal *)signalWithValue:(id)input {
    return self.block(input);
}

RACDynamicSignalGeneratorの利用例

ではRACDynamicSignalGeneratorを利用したメソッドrepeatretry:を見ていきましょう。

なお、これらは前回紹介したconcat:catch:を利用しているので、必要ならこちらも参照してください。

repeat

repeatはレシーバのシグナルを永遠に繰り返します。つまり次のようなものが無限に続いているものを作ればよいです。

[[[self concat:self] concat:self] concat:self];

RACDynamicSignalGeneratordefer:を用いれば、上記のような無限の繰り返しのシグナルを作ることができます。

- (RACSignal *)repeat {
    RACSignalGenerator *generator = [RACDynamicSignalGenerator generatorWithReflexiveBlock:^(RACSignal *signal, RACSignalGenerator *generator) {
        return [signal concat:[RACSignal defer:^{
            return [generator signalWithValue:signal];
        }]];
    }];
    return [[generator signalWithValue:self] setNameWithFormat:@"[%@] -repeat", self.name];
}

defer:はシグナルがサブスクライブされるときまでシグナルの生成を遅延してくれるメソッドです。サブスクライブされるときに引数のblockが実行されます。

repeatの場合には、selfがcompleteイベントで終わろうとするときにdefer:のblockが実行されることで、繰り返しでselfがサブスクライブされるようになります。

retry:

retry:はレシーバのシグナルがErrorイベンドであったときに、それを指定回数に繰り返します。つまり、次のようなものが指定回数だけ続いているものを作ればよいです。

[self catch:^(NSError*){
    return [self catch:^(NSError* e){
        return [self catch:^(NSError* e){
            return [RACSignal error:e]
        }]
    }]
}]

これも、RACDynamicSignalGeneratordefer:を用いると実現できます。

- (RACSignal *)retry:(NSUInteger)retryCount {
    return [[RACSignal defer:^{
        RACSignalGenerator *generator = [RACDynamicSignalGenerator generatorWithReflexiveBlock:^(NSNumber *currentRetryCount, RACSignalGenerator *generator) {
            return [self catch:^(NSError *error) {
                if (retryCount == 0 || currentRetryCount.unsignedIntegerValue < retryCount) {
                    return [generator signalWithValue:@(currentRetryCount.unsignedIntegerValue + 1)];
                } else {
                    // We've retried enough times, so let the error propagate.
                    return [RACSignal error:error];
                }
            }];
        }];

        return [generator signalWithValue:@0];
    }] setNameWithFormat:@"[%@] -retry: %lu", self.name, (unsigned long)retryCount];
}

例えば、retry:2としたときの挙動を説明します。

  1. まず、遅延で[generator signalWithValue:@0]が実行されます。

  2. retryCount2currentRetryCount0なので、if文のtrue節が実行されることになります。

    見やすさのために、if文を外して、実際に実行される部分だけにしてみると、

    return [self catch:^(NSError *error) {
        return [generator signalWithValue:@(0 + 1)];
    }];
    

    となります。

  3. ここで、selfがErrorイベントを送信したときにはcatch:のblockが実行されることでsignalWithValue:@1が実行されます。

    retryCountは変化がないので2のまま、currentRetryCount1なので、if文のtrue節が実行されます。

    見やすさのために、if文を外して、実際に実行される部分だけにしてみると、前とあまり変わらないような、

    return [self catch:^(NSError *error) {
        return [generator signalWithValue:@(1 + 1)];
    }];
    

    が得られます。

  4. ここで、さらにselfがErrorイベントを送信したなら、signalWithValue:@2が実行されます。

    retryCountは変化がないので2のまま、currentRetryCount2なので、if文のfalse節が実行されることになります。

    見やすさのために、if文を外して、実際に実行される部分だけにしてみると、

    return [self catch:^(NSError *error) {
        return [RACSignal error:error];
    }];
    

    のようになります。

上記3コードのgeneratorの部分をまとめると、

return [self catch:^(NSError *error) {
    return [self catch:^(NSError *error) {
        return [self catch:^(NSError *error) {
            return [RACSignal error:error];
        }];
    }];
}];

となり、最初に書いたコードと同じになることがわかります。

おわりに

SignalGeneratorについて紹介しました。

RACDynamicSignalGeneratordefer:あたりは理解するまでにちょっと時間がかかりましたが、わかると非常に綺麗なコードだと思えるようになりました (2.0とコードを比べるとよくわかると重います)。

みなさんも3.0が安定版になったときにはぜひとも使ってみてください。

関連リンク

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。