倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

GDK が List に追加している getAt() と putAt() に指定できるインデックス範囲についての絞殺

『GDK の List がこんなに可愛いわけがない。』の List 編1List 編2で、GDK が List インターフェースに追加しているメソッド getAt(), putAt() を見てきました。 そのときに API を見たりサンプルコードを書いたりしていて思ったのですが、インデックスを指定して要素を取得もしくは設定する getAt() と putAt() は、指定できるインデックスについて対応関係がある(というか同じインデックスの型を指定できる)のが自然だと思うんですが、一部(半分)がちょっと指定できる型にズレがあって妙なことになっております。 その辺りについて考察というか、同じインデックスを指定した場合の挙動のズレみたいなの見ていきたいと思います。 普通に使う分には特に問題ないので重箱の隅を突つく感が否めませんが。

f:id:waman:20130628013258p:plain

GDK が List インターフェースに追加している getAt(), putAt() メソッドには以下のようなシグニチャがあります:

// int
Object getAt(int idx)
void putAt(int idx, Object value)

// EmptyRange
List getAt(EmptyRange range)
void putAt(EmptyRange range, Object value)
void putAt(EmptyRange range, Collection value)

// Range / IntRange
List getAt(Range range)
void putAt(IntRange range, Object value)
void putAt(IntRange range, Collection col)

// Collection / List
List getAt(Collection indices)
void putAt(List splice, Object value)
void putAt(List splice, List values)

以下では putAt() の第2引数としては Object のものを扱います。

int 値のインデックス

int 値でインデックスを指定するものは問題ないでしょう。 負のインデックスは末尾から数えるとかありましたけど。

def list0 = 'abcd'.collect()
assert list0[1] == 'b'

list0[2] = 'x'
assert list0 == 'abxd'.collect()

// サイズ以上のインデックス
def list1 = 'abcd'.collect()
assert list1[6] == null

list1[6] = 'x'
assert list1 == ['a', 'b', 'c', 'd', null, null, 'x']

// 負のインデックス
def list2 = 'abcd'.collect()
assert list2[-1] == 'd'

list2[-3] = 'x'
assert list2 == 'axcd'.collect()

EmptyRange のインデックス

次の EmptyRange でインデックスを指定するものは、getAt() は空リストを返し、putAt() はその位置への挿入をするのでした。

// getAt(int)
def list0 = 'abcd'.collect()
assert list0[0..<0] == []
assert list0[1..<1] == []

// putAt(int, Object)
list0[0..<0] = 'x'
assert list0 == 'xabcd'.collect()

list0[2..<2] = 'y'
assert list0 == 'xaybcd'.collect()

同じ EmptyRange でも 0..<0, 2..<2 を指定した場合に putAt() の挙動が異なるのが違和感あるような・・・

Range / IntRange のインデックス

さて、この辺りからが getAt() と putAt() でインデックスの型が違ってるメソッド。 ちなみに Range はインターフェースで、IntRange は Range を実装したクラスです。 Range を実装したクラスは他に(groovy.lang パッケージ内で)EmptyRange, ObjectRange というのがあります:

package groovy.lang

interface Range extends java.util.List
class EmptyRange  extends java.util.AbstractList implements Range
class IntRange    extends java.util.AbstractList implements Range
class ObjectRange extends java.util.AbstractList implements Range

これらはコンストラクタで境界 (from, to)*1 の値を指定してインスタンス化します。 また、これらのクラスは不変、すなわち要素を変更することはできません*2。 これを踏まえて、まずは通常の使い方。 次のサンプルでは IntRange オブジェクト (0..2) でインデックスを指定しています。 IntRange は Range のサブクラスなので、これはまぁ意図された普通の使い方でしょう:

assert (0..2) instanceof IntRange

def list0 = 'abcd'.collect()

// getAt(Range)
assert list0[0..2] == 'abc'.collect()

// putAt(IntRange, Object)
list0[0..2] = 'x'
assert list0 == 'xd'.collect()

さて。 getAt() の引数は Range で putAt() の引数が IntRange なので、Integer を要素に持つ ObjectRange でインデックスを指定してやるとどうなるでしょう? 実際にやってみましょう:

def index = new ObjectRange(0, 2)
assert index instanceof Range
assert !(index instanceof IntRange)    // IntRange オブジェクトではない

// getAt(Range) が呼び出されている
def list0 = 'abcd'.collect()
assert list0[index] == 'abc'.collect()

// putAt(List, Object) が呼び出されている!
list0[index] = 'x'
assert list0 == 'xxxd'.collect()    // さっきは 'xd'.collect() になってた

getAt() の方はインデックスの型が Range なので IntRange でも ObjectRange でも挙動は同じでしょうね。 一方、putAt() はインデックスは IntRange 型を要求しているので ObjectRange を渡すとこのメソッドは呼ばれません。 代わりに(Range が List のサブタイプなので) putAt(List, Object) が呼ばれます。 で、putAt(IntRange, Object) はその範囲のサブリストを1つの(第2引数で指定された)オブジェクトに置き換え、putAt(List, Object) はリストで指定された位置の要素をそれぞれ(第2引数で指定された)オブジェクトに置き換えるので、結果が異なります。 今の場合は getAt() の方が正しそう。 putAt() の引数も Range にした方がよいかと*3

Collection / List のインデックス

この型でインデックスを指定する場合、通常は List として指定する用途を想定してるんでしょう:

def list0 = 'abcd'.collect()

// getAt(Collection)
assert list0[1, 3] == 'bd'.collect()

// putAt(List, Object)
list0[1, 3] = 'x'
assert list0 == 'axcx'.collect()

これはまぁ、そのままって感じですかね。 putAt() は前の節で出てきたヤツですが、これだけ書いてみると違和感はないかと。 さて、getAt() ではインデックスの型が List ではなく Collection になっているので、例えば Set でインデックスを指定してみましょう:

def list0 = 'abcd'.collect()

def index = [1, 3] as HashSet
assert list0[index].toSet() == 'bd'.collect().toSet()

これはそのままの挙動といえばそのままなんですが、インデックスを Set で指定したら返り値も List ではなく Set で返して欲しい気がするところ。 ただ、これは Groovy 的には実装が難しいんでしょうね。 そういうメソッドはジェネリクスを使って定義しないといけないけど、Groovy は基本的にジェネリクス無視だし、何より Scala などと違って、List や Set などのインターフェースにデフォルトの実装が伴っていないので、引数の型だけからそれにあったコレクションのインスタンスを作るのが難しい(ユーザー定義のコレクションとか出てきたら基本無理)のが原因でしょう。 今の場合は getAt() のインデックスの型が List でいいんじゃないかと。 ちなみに当然のことながら、putAt() に Set のインスタンスをインデックスとして渡すと MissingMethodException が投げられます。

今回は List に定義されている GDK のメソッド getAt(), putAt() でインデックスの型にズレがあるのを見てきました。 インデックスを範囲のリテラル(1..3 のようなもの)で指定してる分(文)には特に問題がありませんが、シグニチャを見てると違和感が拭えないところ。 このズレが意図的なのか特に気にしてないのかよく分かりませんが、ちょっとコード書いた人を絞めたくなる気分w とは言え、古くからあるメソッドなのでそんなに問題は起きなてないんでしょう。 まぁ、自分が使う分には変な使い方をしないようにしておくのが無難なんでしょうけどね。

プログラミングGROOVY

プログラミングGROOVY


クビシメロマンチスト 人間失格・零崎人識 (講談社文庫)

クビシメロマンチスト 人間失格・零崎人識 (講談社文庫)

*1:EmptyRange の場合は1つの値

*2:UnsupportedOperationException が投げられます。

*3:ObjectRange をコンストラクタからインスタンス化できないようにするという手もあるけど。