きりかノート 3冊め

おあそびプログラミング

Cocoa勉強会#51 発表 - clang trunkの新しいリテラル

clang trunkに導入された、@[obj1, obj2]でNSArrayになるなどの新しいリテラルとdict[key]なんかのアクセサ記法(なんていうのが正しい?)について。役に立つ情報というより雑談のネタ的に話しました。

発表の資料はこちら。

http://www.slideshare.net/kimuraw/objctivec-new-literal

ちゃんとした記事はCocoaの日々情報局の記事「最新 clang で @YESや dict[@"key"] が可能に」をごらんください。

以下内容の説明とか。時間の都合で勉強会では省略したネタも書いています。せっかく調べたんだし。

どこからでてきた話?

Xcodeコンパイラとして利用されているclangオープンソースのプロジェクトとして開発されているんだけど、最近こんなコミットがあった。r152137

Add clang support for new Objective-C literal syntax for NSDictionary,
NSArray, NSNumber, and boolean literals. This includes both Sema and
Codegen support.
Included is also support for new Objective-C container subscripting.

新しいリテラルが導入されたらしい。どんなんだろね。

どんな書きかたができるか

中身のコード追っかけるより、テストがちゃんとあるのでそちらを見るのが簡単。今回は、test/SemaObjC (構文のテスト)や、test/ARCMT (新バージョンへの自動書き換え機能のテスト。XcodeのARC対応にする機能なんかでも使われてる)あたりがいいでしょう。

とくにtest/ARCMTは自動書き換えの前後のコードがテストの入力になるので、.mと.m.resultをdiffツールで見比べるとどう変わるのかがとてもわかりやすい。

たとえばFileMergeで見るとこうなる。

objcmt-subscripting-literals.mと.m.result

objcmt-numeric-literals.mと.m.result

例 - NSArray
 @[ obj1, obj2 ]
 // [NSArray
 //    arrayWithObjects:obj1, obj2, nil]

 ary[1]
 // [ary objectAtIndex:1]

 ary[1] = val
 // [ary replaceObjectAtIndex:1 withObject:val]
例 - NSDictionary
 @[ key1 : val1, key2 : val2 ]
 // [NSDictionary
 //    dictinaryWithObjectsAndKeys:
 //      val1, key1, val2, key2, nil]

メソッドでは値 > キーの順、新しいリテラルではキー > 値の順になってる。どちらも意味的には自然だけどちょっとまぎらわしいね。

 dict[key]
 // [dict objectForKey:key]

 dict[key] = val
 // [dict setObject:val forKey:key]
例 - NSNumber
 @123
 // [NSNumber numberWithInt:123]

 @-123
 // [NSNumber numberWithInt:-123]

 @123.0
 // [NSNumber numberWithFloat:123.0]

 @YES
 // [NSNumber numberWithBool:YES]

@YESはちょっとキモチワルイかも。従来のYESと@の有無で区別しろってのはキツイんじゃないかと思うなあ。型がちがうからコンパイラが警告出すとは思うけど。

obj[idx]のしくみ

ところで、NSArrayとNSDictionaryの新しいアクセサ記法って字面は一緒だよね。

 obj = ary[1];    ary[1] = obj;    // NSArray
 obj = dict[key]; dict[key] = obj; // NSDictionary

これって内部的にはどうやって見分けてるんだろ?考えてみた。

  • 案1: インデックスによって見分けている。obj[idx]のidxが数値(NSInteger, int, long, ...)ならNSArrayとして、idxがオブジェクト(id型)ならNSDictionaryとして扱う。
  • 案2: obj[idx]のobjクラスを判定してNSArrayおよびそのサブクラス、NSDictionaryおよびそのサブクラスかどうかによって判断する。

ようするにインデックスかレシーバのどっちかだよね、きっと。調べるにはどうしたらよいか。

  • obj[idx]のobjをNSArrayやNSDictionary以外の型にして試してみる
    • クラスで判定してるならそもそも通らない
    • idxの型で判定してるならidxを数値、オブジェクトにした場合で変化があるはず

コード書いてコンパイル、動作確認してもいいんだけど、せっかくテストがあるのでそれを利用するのがよさそう。ということでターゲットはtest/SemaObjC/objc-array-literal.mにしよう。

   Index: objc-array-literal.m
   ===================================================================
   --- objc-array-literal.m	(revision 152917)
   +++ objc-array-literal.m	(working copy)
   @@ -38,4 +38,8 @@

      const char *blah;
      NSArray *array2 = @[blah]; // expected-error{{collection element of type 'const char *' is not an Objective-C object}}
   +
   +  NSNumber *obj = @1;
   +  NSLog(@"%@\n", obj[0]);
   +  NSLog(@"%@\n", obj[@"key"]);
    }

ということで実行。事前にClang - Getting Startedの手順でtrunkをコンパイル、buildディレクトリで一度はmake testを通しているものとします。

   % pwd
   .../build/tools/clang/test
   % make TESTDIRS=SemaObjC
   Making Clang 'lit.site.cfg' file...
   Making Clang 'Unit/lit.site.cfg' file...
    :
   --
   Exit Code: 1
   Command Output (stderr):
   --
   error: 'error' diagnostics seen but not expected:
     Line 43: expected method to read array element not found on object of type 'NSNumber *'
     Line 44: expected method to read dictionary element not found on object of type 'NSNumber *'
   2 errors generated.
   --

   ********************
   Testing Time: 25.28s
   ********************
   Failing Tests (1):
       Clang :: SemaObjC/objc-array-literal.m

     Expected Passes    : 357
     Unexpected Failures: 1
   make: *** [all] Error 1

ここ注目。

     Line 43: expected method to read array element not found on object of type 'NSNumber *'
     Line 44: expected method to read dictionary element not found on object of type 'NSNumber *'

"expected method"ってことはメソッドがあればいいのか。

   Index: objc-array-literal.m
   ===================================================================
   --- objc-array-literal.m        (revision 152917)
   +++ objc-array-literal.m        (working copy)
   @@ -21,6 +21,10 @@

    @interface NSNumber
    + (NSNumber *)numberWithInt:(int)value;
   +- (id)objectAtIndexedSubscript:(NSUInteger)index;
   +// - (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index;
   +- (id)objectForKeyedSubscript:(id)key;
   +// - (void)setObject:(id)object forKeyedSubscript:(id)key;
    @end

    @interface NSArray <NSFastEnumeration>
   @@ -38,4 +42,8 @@

      const char *blah;
      NSArray *array2 = @[blah]; // expected-error{{collection element of type 'const char *' is not an Objective-C object}}
   +
   +  NSNumber *obj = @1;
   +  NSLog(@"%@\n", obj[0]);
   +  NSLog(@"%@\n", obj[@"key"]);
    }

ということでもいちどテスト。

   % make TESTDIRS=SemaObjC
   Making Clang 'lit.site.cfg' file...
       :
     Expected Passes    : 358
   %

うむ。

ということで、必要なメソッドさえ実装していれば obj[idx] や obj[idx] = val という書きかたが利用できるみたい。fast enumerationなんかと同じだね。(参考: ダイナミックObjective-C Fast Enumeration (4) - Fast Enumerationに対応するクラスの実装

rubyはじめたころ、def self[](idx) にびっくりしたことを思い出したり。

(2012-03-20 追記)Big Nerd Ranch Weblogの記事"Objective-C Literals, part 2"の、Making your own indexable collectionsからのところにサンプルコード含め、詳しく書かれています。dict[key]での階層キー(Key Value CodingでいうところのvalueForKey:@"key1.key2.key3")でのアクセスなど。ぜひ読んでね。

ぼく個人の感想

RubyとかPythonに慣れてる人にとっては自然というか、従来の[NSArray arrayWithObjects:]とかがダルすぎるってとこでしょうか。ライブラリももちろんだけど、リテラルって言語のポイントとしてけっこー重要だよね。

dot syntaxなんかはgcc時代に導入されたものだけど、clangを手にしてからのAppleはすごく突っ走ってるよね。ARCとか。まー今回の件は妥当というか正しい方向だと思ってる。

あとちょっと気になるのが、従来のObjective-C/Cocoaのアプローチは

  • オブジェクト、非オブジェクトの使い分け。たとえばintやfloatを多用。
  • immutable, mutable
  • NSArray, NSSet

というように細かく設計されてる傾向があるんだよね。たぶんパフォーマンスのためだと思うんだけど。これってリテラルの強化とちょっと相性がよくない(Rubyの「大クラス主義」のほうが向いてる)と思ってて、

  • リテラルで定義したオブジェクトはmutableであるべき
  • NSNumberと数値が直接比較できないのはがっくり

みたいな話がでてくるような気がします。次に書くMLでの反応ではすでにそんな傾向が見えてたり。

objc-language MLでの反応

objc-language MLのほうながめてたらこんな意見が。

  • mutable Arrayのリテラルはないの? [@[elem1, elem2, ..] mutableCopy] とかダサいし。
    • @[elem1, elem2, .. @]とかどう?
  • NSSet, NSCoutedSetのリテラルもほしい!
    • @()や@({})でどう?

いやおまえら、増やせばいいってもんじゃねーだろ。ややこしくなるじゃん。

勉強会での反応