読者です 読者をやめる 読者になる 読者になる

24/7 twenty-four seven

iOS/OS X application programing topics.

複雑な正規表現を分かりやすくするライブラリ VerbalExpressions の Objective-Cバージョンを書きました

Mac Objective-C iPhone

https://github.com/VerbalExpressions/ObjectiveCVerbalExpressions
↑ 本家にマージされました。

https://github.com/kishikawakatsumi/ObjectiveCVerbalExpressions

概要

VerbalExpressions はメソッドチェーンとわかりやすい名前を使って、正規表現を読みやすくしようという試みです。
↓ オリジナルはJavaScriptのライブラリのようです。
https://github.com/VerbalExpressions/JSVerbalExpressions


iOS Dev Weeklyの106号でObjective-Cの移植はまだ無いみたいに書いてあったので、やってみました。
(実際は2つほど先に書かれたものがありました)

↓ 私が書いた Objective-C 版のライブラリを使うと下記のように記述できます。

// Create an example of how to test for correctly formed URLs
VerbalExpressions *verEx = [VerbalExpressions instantiate:^(VerbalExpressions *ve) {
    ve.startOfLine(YES)
    .then(@"http")
    .maybe(@"s")
    .then(@"://")
    .maybe(@"www")
    .anythingBut(@" ")
    .endOfLine(YES);
}];

最初のインスタンス化は Blocks 付きのメソッドを使わずに普通にインスタンス化することもできます。
(alloc] init] や new を使ってもいいでしょう)

VerbalExpressions *verEx = [VerbalExpressions expressions];
verEx.startOfLine(YES).then(@"http").maybe(@"s").then(@"://").maybe(@"www").anythingBut(@" ").endOfLine(YES);

仕組みについて

通常 Objective-C とこういったメソッドチェーンを多用する、いわゆる「流れるようなインターフェース (fluent interface)」はあまり相性がよくありません。

例えば、別の人の書かれた Objective-C 版の VerbalExpressions ですが、普通にメソッドチェーンを使って書くと下記のようになります。
https://github.com/sakiwei/ObjectiveCVerbalExpressions

// url matches
VerbalExpressions *tester = [[[[[[[VerEX() startOfLine] then:@"http"] maybe:@"s"] then:@"://"] maybe:@"www."] anythingBut:@" "] endOfLine];

↑ 読みやすくないこともないですが、書きやすくはないですよね。
普通の Objective-C のメソッド呼び出しは両側にカッコを追加して行かなければならないので、チェーンを追加しようとすると最初に戻って開きカッコを追加したりしないといけないのでこのやり方だと、考えながら書くっていうのが難しいです。


なので今回は Blocks を利用して他のライブラリと同様にドットでチェーンできるようにしてみました。

VerbalExpressions *verEx = [VerbalExpressions expressions];
verEx.startOfLine(YES).then(@"http").maybe(@"s").then(@"://").maybe(@"www").anythingBut(@" ").endOfLine(YES);


方法としては VerbalExpressions クラスに自分自身を戻り値として返すブロックをプロパティとして定義しています。

@interface VerbalExpressions : NSObject

@property (nonatomic, readonly) VerbalExpressions *(^startOfLine)(BOOL enable);
@property (nonatomic, readonly) VerbalExpressions *(^endOfLine)(BOOL enable);
@property (nonatomic, readonly) VerbalExpressions *(^find)(NSString *value);
@property (nonatomic, readonly) VerbalExpressions *(^then)(NSString *value);
@property (nonatomic, readonly) VerbalExpressions *(^maybe)(NSString *value);
@property (nonatomic, readonly) VerbalExpressions *(^anything)();
@property (nonatomic, readonly) VerbalExpressions *(^anythingBut)(NSString *value);


プロパティに実装は下記のようになっていて、プロパティを参照するとブロックが実行されて正規表現が組み立てられるというしくみです。そしてこのブロックは自分自身のインスタンスを返すので、その戻り値に対してドットでチェーンできる、というように書かれています。

- (VerbalExpressions *(^)(NSString *))maybe
{
    return ^VerbalExpressions *(NSString *value) {
        value = [self sanitize:value];
        self.add([NSString stringWithFormat:@"(%@)?", value]);
        return self;
    };
}


この方法の課題としてはオーバーロードができないので、例えばオリジナルの JS ライブラリでは `startOfLine()` と `startOfLine(bool)` という 2 つのメソッドがあるのですが、Blocks のプロパティだとどちらも同じ名前になってしまうので、引数付きのものだけ用意されています。


実際に有用かどうかは使いどころによると思いますが、おもしろい試みだと思いますので、ぜひ使ってみてください。
バグレポートや Pull Request もお待ちしています。