Objective-C
AdventCalendar
iOS

Objective-Cでドット繋ぎでメソッドチェインを行う

More than 5 years have passed since last update.

Objective-C Advent calendar 22日目。

NSArrayなんかにmapやfilterといったメソッドをカテゴリで生やして、それぞれのメソッドを繋ぎあわせて処理をしたいといった時(そんな時があるのか甚だ疑問ではありますが)、通常のメソッド呼び出しだとこんな実装になると思います。

@interface NSArray (functional)
- (NSArray*)filter:(BOOL(^)(id))block;
- (NSArray*)map:(id(^)(id))block;
@end
@implementation NSArray (functional)

- (NSArray*)filter:(BOOL(^)(id))block
{
    NSMutableArray *arr = [NSMutableArray array];
    for(id obj in self) {
        if(block(obj)) {
            [arr addObject:obj];
        }
    }
    return arr;
}

- (NSArray*)map:(id(^)(id))block
{
    NSMutableArray *arr = [NSMutableArray array];
    for(id obj in self) {
        id ret  = block(obj);
        if(ret) {
            [arr addObject:ret];
        }
    }
    return arr;
}

@end

で、コレを使って配列の要素を2倍して、特定条件でフィルタしたいとすると、

    NSArray *ret = [[@[@100, @200, @300, @400] map:^id(id num) {
        return [NSNumber numberWithInt:[num intValue] * 2];
    }] filter:^BOOL(id num) {
        return [num intValue] > 400;
    }];
    NSLog(@"%@", ret);  // ==> (600, 800)

といった感じになります。メソッド2つ噛ませる程度ならコレでも良いんですが、これが3つも4つもとなると、

    [[[[@[@100, @200, @300, @400] map:^id(id num) {
        // アレして
    }] filter:^BOOL(id num) {
        // コレして
    }] map:^id(id num) {
        // ソウして
    }] filter:^BOOL(id num) {
        // ファイヤー
    }];

といった感じでObjective-Cの構文上メソッドチェインしようとすると、先頭行辺りに括弧が集結してしまい、非常に可読性が悪くなります。

そこでpropertyとblockを使ったメソッドチェインの実装方法を紹介します。

Objective-Cのpropertyはobject.propertyといった形でオブジェクトにドットを付けた記述方法でアクセサメソッドを実行することができます。なので、上記のmapfilterメソッドもプロパティの形で実装すれば、括弧で囲まずにメソッドを実行することができるわけです。

しかし、通常のgetterは引数を取ることができず、逆にsetterは戻り値を設定することができないたメソッドを繋げることができません。

そこで、blockを返すgetterを宣言します。

@interface NSArray (functional)
@property (readonly) NSArray*(^filter)(BOOL(^)(id));
@property (readonly) NSArray*(^map)(id(^)(id));
@end

実装はこんな感じ。(汚い)

@implementation NSArray (functional)

- (NSArray *(^)(BOOL (^)(id)))filter
{
    return ^(BOOL (^block)(id)) {
        NSMutableArray *arr = [NSMutableArray array];
        for(id obj in self) {
            if(block(obj)) {
                [arr addObject:obj];
            }
        }
        return arr;
    };
}

- (NSArray *(^)(id (^)(id)))map
{
    return ^(id (^block)(id)) {
        NSMutableArray *arr = [NSMutableArray array];
        for(id obj in self) {
            id ret  = block(obj);
            if(ret) {
                [arr addObject:ret];
            }
        }
        return arr;        
    };
}

blockの呼び出しはC言語の関数呼び出しと同じ記述方法のため、arr.map().filter()といった形でドットを使って数珠つなぎにメソッドを実行していくことが可能になります。

使用例。

    NSArray *ret = @[@100, @200, @300, @400]
    .map(^id(id num) {
        return [NSNumber numberWithInt:[num intValue] * 2];
    })
    .filter(^BOOL(id num) {
        return [num intValue] > 400;
    });
    NSLog(@"%@", ret);  // ==> (600, 800)

ドットで繋げていくだけなので、もう括弧の呪縛に悩む必要はありませんね!

ちなみにこんな感じの実装で有名なのはunderscore.mですね。
拙作のSTDeferredでも最近似たような実装を入れましたー(ステマ)