昨日のScalaハマリポイント: 型パラメータAを含むSeq[A]をtoArrayするときには実行時型が必要

例えば、次のようなSchoolクラスがあったとする。

gist.github.com

このクラスはStudentもしくはその子クラスのコレクションをメンバに持っている。このクラス設計が嬉しいのは、コレクションの要素型がパラメータになっているおかげで、もし全ての生徒が男子であればstudentsSeq[Boy]となり、このように作られたboysSchoolに対してはBoyだけがもつメンバfightingInstinctにアクセスできることだ。

ただし、このようなクラス設計をした時にちょっとハマってしまうのが、 Schoolクラスの内部でSeq[T]Arrayに変換しようとしたときに、以下のようにコンパイラに怒られることだ。

Error: No ClassTag available for T
  def studentsAsArray = students.toArray
                                 ^
Error: not enough arguments for method toArray: (implicit evidence$1: scala.reflect.ClassTag[T])Array[T].
Unspecified value parameter evidence$1.
  def studentsAsArray = students.toArray
                                 ^

一方で、ShoolTestboysSchool.students.toArrayとするぶんには問題ない。 このような現象に初めて出会うと、何のことだかさっぱりだ。

何故このようなことが起こるのかというと、ScalaのArrayは実際のところJavaのArrayであり、 JavaのArrayはプリミティブ型に対して最適化された個別のクラスを用意していて、それ以外の参照型に対するArrayとクラスが違うからである。 つまり、Arrayを作るときにこれらのクラスを作り分けるために型Tがプリミティブ型か参照型かを知る必要があるわけだが、 コンパイル時の型消去のために、Schoolクラスの内部ではTが参照型([T <: School])であることを知ることができない。 そのため、一体どのArrayを作ればいいかわからない状態に陥ってしまう。

この問題を回避するために、JavaやScalaには実行時に型情報を取得する実行時リフレクションという機構がある。 Scala 2.11では、ClassTag という オブジェクトを使ってコンパイル時に消去されるクラス型を保持しておき、実行時に取得することができる。

上記の問題の場合、以下のように書くことでTのクラス型を保持し、実行時に知ることができるため、無事参照型のArrayを作ることできるということになる。

gist.github.com

ClassTagの詳細については、ClassTagトレイトのソースコードも参考になる。

参考:

  1. stackoverflow.com

  2. github.com