Collection plug-in

はじめに

Java 言語に足りないとよく言われる言語機能の1つに、 parameterized class があります。 GJ のように Java 言語に parameterized class の機能を追加する提案は いくつか存在しますが、標準仕様にはいまだ取り込まれていません。

GJ は非常にうまく設計されていると思いますが、 Java プログラマーの視点から見ると、いくつか不満が残ります。 例えばループ構文の拡張は行っていないため、 コレクションの要素に対するループの記述の 繁雑さは Java 言語からそのまま引き継いでいます。 また、 int などの基本型を型パラメタに指定できません。 STL と違い、 ローカル変数を宣言して初期化する時に、 長い型名を2度書かなければならないのもめんどうです。

多くの人が parameterized class を必要としていますが、 実は、そのうちのほとんどの人は、静的な型チェックを行ってくれる コレクションライブラリがあれば十分なのではないでしょうか? もともとJava 言語には T[] という組み込みの parameterized type が1つ ありますが、それプラス、よく使う parameterized type が 数個あれば、実用上は十分だと思います。 この方法ならば、 Java 言語の大きな特長である「シンプルさ」も、 あまり損なわずにすみます。

このような設計方針のもとに、厳選した3つの parameterized type を 言語の組み込み型として提供する Collection plug-in を設計・実装しました。 Collection plug-in は、以下の特徴を持ちます。

基本事項

パラメタつきの型(Vec<T>、Table<Key,Value>、Iter<T>、 NullOr<T>)は、 型名が使える全ての場所で使うことができます。 具体的には、ローカル変数宣言、フィールド宣言、 メソッドの返値と引数の宣言などです。

型パラメタには、 int 型か参照型( class, interface, T[] またはコレクション型) だけが許されます。

コレクションに対するメソッド呼び出しの引数と返値は、 ちゃんとコンパイル時に型チェックされます。

型パラメタに int 型を指定した場合は、 コレクションに対するメソッド呼び出しの引数と返値は、 必要に応じて自動的に Integer 型に wrap/unwrap されます。

なお、コレクション型をネストするときは、2つの ">" の間に必ず スペースを入れてください。そうしないとコンパイラは構文解析できず、 シンタックスエラーになります。

可変長配列 Vec<T>

Vec<T> は、可変長配列です。 特定のインデックスに対応する要素の代入・参照が O(1) で行えます。 再後尾の要素の追加・削除が行え、スタックとしても用いることができます。 メソッド toArray() によって、 T[] 型の配列に変換することもできます。

これは Vec<T> の使用例です。

{
  Vec<String> vec = {};  // new Vec<String>() の値で初期化。
  vec.push("aaa");
  vec.push("bbb");
  vec.push("ccc");
  System.out.println(vec.get(2));  // ccc が出力される。
  foreach (String s in vec){
    // aaa, bbb, ccc が順に出力される。
    System.out.println(s);
  }
  vec.put(2, "xxx");  // 要素の変更。
  foreach (int index, String s in vec){  // 2変数 foreach
    // 0:aaa, 1:bbb, 2:xxx が順に出力される。
    System.out.println(index+ ":"+ s);
  }
  System.out.println(vec.size());  // 3 が出力される。
  System.out.println(vec.pop());   // xxx が出力される。
  System.out.println(vec.size());  // 2 が出力される。
}

Vec<T> の初期化式として、 {} が使えます。 これは、 new Vec<T>() 書いたのと同じ意味です。 {} の中に要素を並べることもできます。

Vec<T> の値に対する foreach は、効率を意識して実装されています。 T[] に対する for ループとほとんど変わらない速度で ループすることができます。 (まだ計測はしていません。) また、Vec<T> の値に対する foreach は、 heap を消費しません。

NullOr<T>型とnullチェック構文 ifNull

「正常な値またはは null 」を返すようなメソッドは多くありますが、 そのようなメソッドの返値のnullチェックをプログラマーに強制させるための型が NullOr<T>型です。

次のプログラムはこの型の使用例です。

void test(){
  String s = find("a") ifNull { s = "default"; };
  ...
}
NullOr<String> find(String key){
  if (...) {
    ...
    return str;
  } else {
    return null;
  }
}

NullOr<T>型の値は、ifNull 構文を使って T 型に変換しないと、 T 型の値として使えません。 つまり、うっかり null かどうかのチェックを忘れると コンパイル時エラーになります。 NullOr<T> 型の変数への代入、 return 、引数渡しはできます。

NullOr<T> は Object 型の変数に代入できます。

NullOr<T> 型の変数へは、 NullOr<S>, S, null 型の値を 代入できます。 ただし S は T の subtype (か T 自身)です。

現在のところ型パラメタ T には、 reference 型と int のみが許されます。 (int 型を用いた場合は、実装上は自動的に Integer 型に wrap されます。)

ifNull 構文には2つのタイプがあり、それぞれ 次のようにマクロ展開されます。

        { T var = exp ifNull block; 後に続く文; }
->
        { T var;
          T tmp = exp;
          if (tmp != null)
            var = tmp;
          else
            block
          後に続く文;
        }
        var = exp ifNull block;
->
        { T tmp = exp;
          if (tmp != null)
            var = tmp;
          else
            block
        }

つまり前者のタイプの場合、 宣言された変数のスコープは、 ifNull のブロックの中および、 ifNull の後に続く文にまで及びます。

ifNull 構文で宣言した変数のデフォルト値を Block の中で設定し忘れると、 コンパイルエラーになります。 例えば次のプログラムをコンパイルすると、

 NullOr<int> x = 123;
 int y = x ifNull { /* do nothing */ };
 System.out.println(y);
javac のフェーズで次のような エラーメッセージが出ます。
Test.java:19: Variable y may not have been initialized.
      (System.out).println(y); 
                           ^
1 error

ハッシュ表 Table<Key,Value>

これは Table<Key,Value>の使用例です。

{
  Table<String,int> table = {};
  table.put("aaa", 111);
  table.put("bbb", 222);
  table.put("ccc", 333);
  int x1 = table.get("aaa")
        ifNull { throw new Error(); };
  System.out.println(x1);  // 111 が出力される。
  foreach (int val in table.valueIter()){
    System.out.println(val);  // 111, 222, 333 が順に出力される。
  }
  table.put("aaa", 999);  // 要素の変更。
  foreach (String key, int val in table){  // 2変数 foreach
    // aaa:999, bbb:222, ccc:333 が順に出力される。
    System.out.println(key+ ":"+ val);
  }
}

Table<Key,Value> の初期化式として、 {} が使えます。 これは、 new Table<Key,Value>() と 書いたのと同じ意味です。 現在の実装では {} の中に要素を並べることはできません。

table.put(key, value) で値を追加します。 この時、 value が null であれば、このメソッドは NullPointerException を throw します。

table.get(key) で、値を参照します。 このとき、もし key に対応するエントリがハッシュ表になければ、 値 null が返されます。 table.get(key) の返値の静的な型は NullOr<Value>であるため、 返値を型Valueとして使うためには必ずifNull構文を使う必要があります。

Java 2 collection library などでは ハッシュ表にエントリが存在しない場合の処理を忘れると、 実行時に NullPointerException になってしまいやっかいです。 この Table の場合は、null のチェックを忘れると コンパイル時に type error になるため、 潜在的エラーを早期に発見することができます。

イテレータ Iter<T>

Iter<T> は、 vec.iter() 、 table.keyIter() 、 table.valueIter() の 返値です。 現在のところ、これら以外には Iter<T> のインスタンスを 作る方法はありません。

Iter<T> には次の2つのメソッドがあります。 使い方は、 Java 2 collection library の Iterator と同じです。(もちろん明示的キャストは不要です。)

        boolean hasNext()
        T next()

イテレータを使っている間は、もとになるコレクションの値を 変更してはいけません。それを行った場合の動作は未定義とします。

ループ構文 foreach

1変数 foreach

foreach (Value val in exp) { ... }

exp の型は、int, T[], Vec<T>, Iter<T> のいずれかでなければなりません。

exp は、ループに入る前に1度だけ評価されます。

2変数 foreach

foreach (Key key, Value val in exp) { ... }

exp の型は、 T[], Vec<T>, Table<Key,Value> のいずれかでなければなりません。

exp は、ループに入る前に1度だけ評価されます。

break, continue

foreach 内部での break 文、 continue 文は期待した通りに動きます。 ラベル付き break も期待した通りに動きます。 例:

found:
  foreach (Vec<String> vec in vecOfVec){
    foreach (String s in vec){
      if (s.equals(keyString)) break found;
    }
  }

現在の実装では、ラベル付き continue 文は期待した通りに動きません。 (EPP を通したソースを javac するフェーズでコンパイルエラーになります。) これはバグです。将来直す予定です。

要素の変更

foreach でループしている間、 以下の場合に限り、ループ対象のデータ構造の要素を変更することができます。

これら以外の方法で、 foreach のループ対象の コレクションの要素を変更してはいけません。 変更を行った場合の動作は、未定義とします。

int 型の wrap/unwrap

型パラメタに int を指定することができます。

int 型は実装上は Integer に wrap/unwrap されますが、 プログラマーからはそれは見えません。

-10 <= x < 1024 の間の x に対応する Integer はキャッシュされているので、 wrap するときに heap からメモリを allocate することはありません。 キャッシュされる範囲は、次のようにしてカスタマイズすることが可能です。

jp.go.etl.epp.util.Wrapper.setCacheRange(min, max);

int 以外の基本型の wrap/unrwap は、現在のところ実装されていません。

print メソッド

Vec と Table は print(String) というメソッドを持っていて、 デバッグ用に使えます。

具体例:

Table<String, Vec<String> > table = {};
Vec<String> vec1 = {"aaa", "bbb", "\n"};
Vec<String> vec2 = {"\"\t\""};
table.put("key1", vec1);
table.put("key2", vec2);
Vec<String> v = table.print("table:").get("key1")
  ifNull{ throw new Error(); };

これを実行すると、次のような出力が標準エラー出力に出ます。

table:
{
 {
  "key2",
  {
    "\"\t\"",
  }
 }, {
  "key1",
  {
    "aaa",
    "bbb",
    "\n",
  }
 }
}

ユーザ定義のクラスが要素にある場合も見やすく print するようにカスタマイズすることができます。
(方法については、チュートリアルプログラムの lib/Tutorial.java の中の t.collection.printFunction を見てください。)

将来の課題

Generics(GJ) との統合

近いうちに Java 言語の標準機能に generics が入るようですが、 将来的には Collection plug-in は generics の機能と統合したいと思います。

>> の問題

現在の実装では、g++ と同様、パラメタ付きクラスがネストしたときは、 Vec<Vec<T>> ではなく、 Vec<Vec<T> > のように2つの >> の間にスペースを 入れなければなりません。 generics の仕様ではスペースは不要であり、 Collection plug-in も それに合わせたいと思っています。

文法

Type <original alternatives> Vec < Type > Table < Type , Type > Iter < Type > NullOr < Type >

Statement <original alternatives> foreach ( Type Identifier in Expression ) Block # Expression の型は int, T[], Vec<T>, Iter<T> foreach ( Type Identifier , Type Identifier in Expression ) Block # Expression の型は、 T[], Vec<T>, Table<Key,Value> Identifier = Expression ifNull Block ; Type Identifier = Expression ifNull Block ;

リファレンスマニュアル

主なコンストラクタとメソッド

Vec<T> のコンストラクタ Vec<T>() Vec<T>(Object[]) Vec<T>(Vector)

Vec<T> のメソッド void push(T) T pop() T top() void put(int, T) T get(int) int size() T[] toArray() Iter<T> iter() Vec<T> print(String)

Table<Key,Value> のコンストラクタ Table<Key,Value>()

Table<Key,Value> のメソッド void put(Key, Value) // ただし null は put できない。 NullOr<Value> get(Key) void remove(Key) boolean containsKey(Key) Iter<Key> keyIter() Iter<Value> valueIter() Table<Key,Value> print(String)

Iter<T> のメソッド boolean hasNext() T next()

Vec のイディオム

// Vec の各要素に関数 f を適用して別の Vec を作る。 Vec<T> newVec = {}; foreach (T x in oldVec) { newVec.push(f(x)); }

// vec1 のうしろに vec2 を追加 foreach (T x in vec2) { vec1.push(x); }

// Vec の中から条件 p を満たす最初の要素を検索 T val = null; foreach (T x in vec) { if (p(x)) { val = x; break; } } if (val == null) { ... } else { ... }

// Vec の中の条件 p を満たす要素 x を f(x) に置換 foreach (int index, T x in vec) { if (p(x)) { vec.put(index, f(x)); } }

Table のイディオム

// デフォルトの値 Value val = table.get(key) ifNull { val = defaultValue; };

// デフォルトの値をテーブルに入れる Value val = table.get(key) ifNull { val = defaultValue; table.put(key, val); // 忘れがち。注意! };

// 必ずエントリが存在する場合 Value val = table.get(key) ifNull { throw new Error("fatal"); };

// エントリが存在する場合だけ何か処理 Value val = table.get(key) ifNull { return; }; ...

// エントリが存在しない場合だけ何か処理 if (! table.containsKey(key)) { ... }

// エントリが存在する場合と存在しない場合にそれぞれ何か処理 Value val = table.get(key) ifNull { val = null; }; if (val == null) { ... } else { ... }

// すべての key に対してループ foreach (Key x in table.keyIter()) { ... }

Table<Key,Vec<Value> > のイディオム

// key に対応する Vec に値を追加 Vec<Value> vec = table.get(key) ifNull { vec = new Vec<Value>(); table.put(key, vec); // 忘れがち。注意! }; vec.push(val);


mj-logo
Last updated: May 17 17:16:46 2002