文字列トップ可変なコレクションの具象クラス配列目次

配列

配列(Array)はScalaでは特別なコレクションの一種です。 その一方でScalaの配列はJavaの配列と一対一で対応します。 つまり、Scalaの配列Array[Int]はJavaのint[]として表現され、Array[Double]はJavaのdouble[]として表現され、Array[String]はJavaのString[]として表現されます。 しかし同時に、Scalaの配列はJavaのものよりもより多くのものを提供します。 まず、Scalaの配列は総称的にできます。 つまり、Tが型パラメータや抽象型であるようなArray[T]を扱えます。 次に、Scalaの配列はScalaの列と互換性があり、Seq[T]が要求されているところにArray[T]を渡せます。 最後に、Scalaの配列は列の全ての演算をサポートします。 これは実際に動いている例です:

scala> val a1 = Array(123)
a1: Array[Int] = Array(1, 2, 3)
scala> val a2 = a1 map (_ * 3)
a2: Array[Int] = Array(3, 6, 9)
scala> val a3 = a2 filter (_ % 2 != 0)
a3: Array[Int] = Array(3, 9)
scala> a3.reverse
res1: Array[Int] = Array(9, 3)

Scalaの配列がJavaの配列と同じように表現されているとすると、 これらの追加の機能はScalaでどのようにサポートされているのでしょうか。 より詳しく言うとこの質問への答えはScala 2.8とそれ以前で異なります。 以前は、Scalaコンパイラはボックス化および非ボックス化と呼ばれるプロセスで必要になると、いくぶん「魔法のように」Seqにラップしたり戻したりしていました。 これの詳細はとても複雑で、特に総称的な型Array[T]を作ろうとしているときは複雑でした。 不可解なコーナーケースがあり配列演算のパフォーマンスは全く予測不可能でした。

Scala 2.8では設計はずっと簡単です。 コンパイラの魔法はほとんど無くなりました。 その代わりにScala 2.8での配列の実装は系統だった暗黙の変換を使います。 Scala 2.8では配列は列であるとは装いません。 ネイティブ配列のデータ型の表現はSeqのサブタイプではないので、本当に装えないのです。 その代わりに、配列と、Seqの子クラスであるscala.collection.mutable.WrappedArrayクラスの間において、暗黙の「ラップする」変換があります。 動いている様子を見ましょう:

scala> val seq: Seq[Int] = a1
seq: Seq[Int] = WrappedArray(123)
scala> val a4: Array[Int] = s.toArray
a4: Array[Int] = Array(123)
scala> a1 eq a4
res2: Boolean = true

上記のインタラクションは配列からWrappedArrayへの暗黙の変換があるため、配列が列と互換性があることを示しています。 反対方向に行く場合、つまりWrappedArrayからArrayへは、Traversableで定義されているtoArrayを使えます。 上記の最後のREPL行はラップしてからtoArrayで戻すと最初のものと同一の配列になるということを示しています。

配列に対して適用される暗黙の変換はもう1つあります。 この変換は列の全てのメソッドを配列に単に「追加」しますが、配列を列には変換しません。 「追加」とは列の全てのメソッドをサポートする他のオブジェクトArrayOpsに配列がラップされることを意味します。 典型的にはこのArrayOpsは短命です。列のメソッドの呼び出し後にはアクセスできなくなり、領域はリサイクルされます。 最近のVMはしばしばこのオブジェクトの生成を完全に回避します。

配列に対する2つの暗黙の変換の違いを次のREPL対話に示します:

scala> val seq: Seq[Int] = a1
seq: Seq[Int] = WrappedArray(1, 2, 3)
scala> seq.reverse
res2: Seq[Int] = WrappedArray(3, 2, 1)
scala> val ops: collection.mutable.ArrayOps[Int] = a1
ops: scala.collection.mutable.ArrayOps[Int] = [I(1, 2, 3)
scala> ops.reverse
res3: Array[Int] = Array(3, 2, 1)

WrappedArrayであるseqreverseを呼ぶとやはりWrappedArrayを返します。 これはラップされた配列はSeqであり、任意のSeqに対するreverseの呼び出しはやはりSeqなので理にかなっています。 その一方、ArrayOpsクラスのopsreverseを呼ぶとSeqではなくArrayを返します。

上記のArrayOpsの例はWrappedArrayとの違いを示すことだけを意図した非常に人工的なものです。 通常はArrayOpsクラスの値を定義はしないないでしょう。 ただSeqのメソッドを配列に対して呼ぶだけです:

scala> a1.reverse
res4: Array[Int] = Array(3, 2, 1)

ArrayOpsオブジェクトは暗黙の変換によって自動的に挿入されます。 そのため上記の行は以下と同じです。

scala> intArrayOps(a1).reverse
res5: Array[Int] = Array(3, 2, 1)

ただしintArrayOpsはその前の例で挿入されていた暗黙の変換です。 ここでコンパイラは上の行で他の暗黙の変換WrappedArrayではなくintArrayOpsをどうやって選んだのかという疑問が湧きます。 なにしろ両方の変換が、入力として指定された条件であるreverseメソッドをサポートすような型に配列をマップするのですから。 この疑問に対する答えは2つの暗黙の変換には優先順位付けられている、というものになります。 ArrayOpsへの変換はWrappedArrayへの変換よりも高い優先順位を持っています。 前者はPredefで定義され、一方後者はPredefが継承するscala.LowPritoryImplicitsで定義されています。 子クラスと子オブジェクトの暗黙の変換は親クラスでの暗黙の変換よりも優先されます。 そのためもし両方の変換が適用可能であればPredefにあるものが選択されます。 同じようなしくみが文字列に対しても働きます。

さて、ここまでで配列が列と互換性があり、どのように列の全ての演算をサポートしているのかが明らかになりました。 では総称性についてはどうでしょうか。 Javaでは型パラメータTに対してT[]とは書けません。 ではScalaのArray[T]はどのように表現されているのでしょうか。 実はArray[T]のような総称的な配列は実行時にはJavaの8種類のプリミティブ型の配列 byte[], short[], char[], int[], long[], float[], double[], boolean[] や任意のオブジェクトの配列であり得るのです。 これら全てに対する実行時の共通の型はAnyRef(すなわちjava.lang.Objectと等価)だけですので、 Scalaコンパイラはこの型にArray[T]をマップします。 実行時にArray[T]型の配列にアクセスする場合、実際の配列型を特定するために型テストをして、その後にJavaの配列に対する正しい配列演算を実行します。 この型テストは配列演算を多少遅くします。 総称的な配列に対するアクセスはプリミティブ型やオブジェクト型の配列に対するアクセスと比べて、3から4倍遅くなると考えてください。 その結果、もし最良のパフォーマンスを得たい場合は、総称的な配列ではなく具体的な配列を使う必要があります。 しかし、総称的な配列を表現できるだけでは十分ではありません。 総称的な配列を作る方法も必要です。 これはより難しい問題で、少々あなたの助けが必要です。 問題をはっきりさせるために、配列を作る総称メソッドを書こうとする例を考えます:

// 間違ったコードです!
def evenElems[T](xs: Vector[T]): Array[T] = {
  val arr = new Array[T]((xs.length + 1) / 2)
  for (i <- 0 until xs.length by 2)
    arr(i / 2) = xs(i)
  arr
}

evenElemsメソッドはベクタの引数xsの偶数番目の要素全てからなる新しい配列を返します。 evenElemsの本体の最初の行では、引数と同じ要素型を持つ配列を作成しています。 そのため実際の型パラメータTに依ってこれはArray[Int]でもArray[Boolean]でも他のJavaのプリミティブ型の配列でも参照型の配列でもあり得るのです。 しかし、これらの型は全て実行時には異なる表現を持ちます。ではScalaのランタイムはどうやって正しいものを選択するのでしょうか。 実は型パラメータTに対応する実際の型は実行時には消されてしまっているので、ここで与えられた情報だけではできません。 そのため上記のコードをコンパイルすると以下のエラーメッセージが出ます:

error: cannot find class manifest for element type T
(参考訳: エラー: 要素型Tのクラスマニフェストが見付かりません)
  val arr = new Array[T]((arr.length + 1) / 2)
            ^

ここで求められているのは、evenElemsの実際の型パラメータが何であるか実行時のヒントを提供して欲しいということです。 この実行時のヒントはscala.reflect.ClassManifest型を持つクラスマニフェストの形を取ります。 クラスマニフェストとは、型の最上位のクラスが何であるか説明する型記述子オブジェクトです。 クラスマニフェストの代わりに、scala.reflect.Manifest型を持ち型の全ての特徴を記述する完全なマニフェストもあります。 しかし配列の作成のためにはクラスマニフェストだけで十分です。

Scalaコンパイラは指示されればクラスマニフェストを自動的に構築します。「指示」とは以下のようにクラスマニフェストを暗黙のパラメータとして要求することです:

def evenElems[T](xs: Vector[T])(implicit m: ClassManifest[T]): Array[T] = ...

代わりのより短い構文をつかうなら、文脈の限定(context bound)を使って型にクラスマニフェストが付属することを要求できます。 これはつまり以下のように型の後にコロンと型名ClassManifestを置くということです:

// これは上手くいく
def evenElems[T: ClassManifest](xs: Vector[T]): Array[T] = {
  val arr = new Array[T]((xs.length + 1) / 2)
  for (i <- 0 until xs.length by 2)
    arr(i / 2) = xs(i)
  arr
}

修正した2通りのevenElemsは全く同じ意味となります。 どちらの場合でもArray[T]を構築する場合、コンパイラは型パラメータTのクラスマニフェストを探索する、つまりClassManifest[T]型の暗黙の値を探索します。 もしそのような値が見付かれば正しい種類の配列を作成するためにマニフェストが使われます。 そうでなければ上記のようなエラーメッセージが現れます。

これはevenElemsメソッドを利用するREPLのインタラクションです。

scala> evenElems(Vector(12345))
res6: Array[Int] = Array(1, 3, 5)
scala> evenElems(Vector("this""is""a""test""run"))
res7: Array[java.lang.String] = Array(this, a, run)

どちらの場合でもScalaコンパイラは要素型(最初はInt、次のはString)のクラスマニフェストを自動的に構築し、evenElemsメソッドの暗黙のパラメータに渡していいます。 コンパイラは全ての具象型についてはクラスマニフェストを構築できますが、もし型引数がクラスマニフェストの無い他の型パラメータであれば構築できません。例えば以下は上手くいきません:

scala> def wrap[U](xs: Array[U]) = evenElems(xs)
<console>:6: error: could not find implicit value for 
 evidence parameter of type ClassManifest[U]
(参考訳: エラー: ClassManifest[U]型の証拠パラメータに対する暗黙の値が見付かりません)
     def wrap[U](xs: Array[U]) = evenElems(xs)
                                          ^

ここで起きたのはevenElemsは型パラメータUの型パラメータを要求したが見付からなかったということです。 この場合の解決方はもちろんさらにUのクラスマニフェストを要求することです。つまり以下は上手く行きます:

scala> def wrap[U: ClassManifest](xs: Array[U]) = evenElems(xs)
wrap: [U](xs: Array[U])(implicit evidence$1: ClassManifest[U])Array[U]

この例はUの宣言における文脈の限定は暗黙のパラメータの略でしかないということを示しており、パラメータはここではevidence$1という名前でClassManifest[U]型です。

まとめると、総称的な配列の作成にはクラスマニフェストが必要です。 型パラメータTの配列を作成する場合は、常にT型に対する暗黙のクラスマニフェストを提供する必要があります。 そのための最も簡単な方法は[T: ClassManifest]のように型パラメータにClassManifestという文脈の限定を付ける方法です。

続いては: 文字列


文字列トップ可変なコレクションの具象クラス配列目次