LL脳がscalaの勉強を始めたよ その68


Scalaコップ本の19章の続きをやっていきますよ。19章は型パラメータについてですが、今回は変位指定アノテーションについてやっていきますよー

ちなみに19章のサンプルとして前回までに構築した純粋関数型待ち行列クラスQueueはこんな感じですネ

// 外部からアクセスすための公開インターフェースとしてのトレイト
trait Queue[T]{
  def head:T
  def tail:Queue[T]
  def append(x:T):Queue[T]
}

// クラス実装自体を格納するオブジェクトです
object Queue{
  // 待ち行列クラスの実装を行う非公開クラスです
  // 実装内容はこれまでのQueueと同じですネ
  private class QueueImpl[T](
    private val leading:List[T],
    private val trailing:List[T]
    // トレイトミックスインです
    ) extends Queue[T]{
      def mirror = {
        if(leading.isEmpty) new QueueImpl(trailing.reverse, Nil)
        else this
      }
      def head:T = mirror.leading.head
      def tail:QueueImpl[T] = {
        val q = mirror
        new QueueImpl(q.leading.tail, q.trailing)
      }
      def append(x:T) = new QueueImpl(leading, x::trailing)
    }
  // オブジェクト生成用のapplyメソッドデス
  def apply[T](xs:T*):Queue[T] = new QueueImpl[T](xs.toList, Nil)
}

変位指定アノテーション

コンストラクター

前回構築したQueueクラスはトレイトを使って実行効率性を保ちつつも内部の動作を隠蔽してやったのですが、残念ながらQueueクラスを型として使えないのでした(´・ω・`)

scala> def test(q:Queue){}
<console>:5: error: trait Queue takes type parameters
       def test(q:Queue){}
                  ^

なんだ、トレイトだめじゃん(´・ω・`)と思ってしまうところなんですが、Queueクラスにパラメータを与えてやると型として使えるという不思議動作だったりするのです(´・ω・`)

scala> def test(q:Queue[String]){}
test: (Queue[String])Unit

このようにトレイトにパラメータを渡すことで型として利用出来るQueueのような存在を型コンストラクター、またはジェネリックトレイトと呼ぶみたいですね。ちなみにQueueはQueue[Int]、Queue[String], Queue[Anyref]なんかの型ファミリーを生成するそうです。

型パラメーターのサブ型

QueueはQueue[String], Queue[Anyref]のような型を生成するわけで、これまでの経験上Queue[String]はQueue[Anyref]のサブ型になりそう…とおもってしまうのですが、実際にはサブ型にならないみたいです。このような要素型の継承関係を無くしてしまう関係を”非変”とよぶみたいですね。

逆にString, Anyref間のサブ型関係をQueue[String], Queue[Anyref]間の関係にも適用したい(共変にする)場合はトレイトの定義を次のようにする必要があるみたいです。

// 型に+を付与することで共変関係を定義します
trait Queue[+T]{
  def head:T
  def tail:Queue[T]
  def append(x:T):Queue[T]
}

共変宣言をすることで、コンパイラ様は作成した型ファミリーに対してサブ型のチェックを導入してくれるそうですな(´・ω・`)

また共変とは正反対にQueue[Anyref]がQueue[String]のサブ型であるYO!という定義をする反変という定義も出来るみたいです。反変は次のように定義しますねー

// 型に-を付与することで反変関係を定義します
trait Queue[-T]{
  def head:T
  def tail:Queue[T]
  def append(x:T):Queue[T]
}


こんな風に共変や反変、非変を指定することを”変位指定”、変位指定するための+や-を変位指定アノテーションと呼ぶみたいですね。こうやって作成した型に対して要素型の関係性を定義してやるんですねc⌒っ゚д゚)っφ メモメモ...

変位指定の例

変位指定の例を見ていきますかねー、まずはサンプルとして次のような独自型Cellを定義してやりますよ。Cellは変位指定アノテーションを付与していないので非変になりますね。

// 非変な独自型を生成します
class Cell[T](init:T){
  // 内部変数を用意します
  private[this] var current = init
  // 内部変数に対するセッターゲッターを定義します
  def get = current
  def set(x:T){current = x}
}

んじゃ、非変な型を試してみましょう

// Cell[String]型の変数を用意します
scala> val c1 = new Cell[String]("abc")
c1: Cell[String] = Cell@11c135c

// 要素のサブ型関係を元にすれば成り立つはず…
scala> val c2:Cell[Any] = c1
// 非変なのでダメでした(´・ω・`)
<console>:6: error: type mismatch;
 found   : Cell[String]
 required: Cell[Any]
       val c2:Cell[Any] = c1
                          ^

それじゃ共変関係にして同じことを試してみますデス

// 共変で定義しますよ
class Cell[+T](init:T){
  private[this] var current = init
  def get = current
  def set(x:T){current = x}
}
// コンパイル時にエラーが発生してしまいました(´・ω・`)
<console>:7: error: covariant type T occurs in contravariant position in type T of value x
         def set(x:T){current = x}
                 ^

見事コンパイルエラーが発生しましたデス(´・ω・`)。これはsetメソッドで場合によってはサブ型の関係性が保持できねーぞ!というコンパイラ様のお怒りの声みたいです。例えばこんなパターンだと型の関係性は破綻しますネ

// Cell型要素がStringデス
val c1 = new Cell[String]("abc")

// Cell要素をAnyにしてみます 
val c2:Cell[Any] = c1

// 要素にIntをセットします
// 元々のString型がInt型になってしまうです
c1.set(1)

// Stringの要素を取り出します 
// すでにInt型の要素になっているのでこれはできませんネ
val s:String = c1.get

上記の破綻パターンは前後の要素型変換はともかく、全体的にみてみると破綻しまくってるというパターンですな(´・ω・`)

変位指定と配列

ちなみにJavaの配列は共変として扱われているらしく、次のようなコードはコンパイラを通過してしまうそうです。

// 上記Scala的Cell型をJavaの配列で書き直してマス
String[] a1 = {"abc"};
Object[] a2 = a1;
a2[0] = new Integer(17)
String a = a1[0]

でも実行時にa2[0]にIntegerが突っ込まれたタイミングでArrayStoreException例外が投げられるとのことです。このように配列を共変にしたのは全ての要素をソートするメソッドを書けるようにしたかったとのことです(´・ω・`)まあJavaジェネリックとやらが登場したせいで、現状では不要な定義らしいですが…まあ昔の名残としてそうなっているとのこと…

…と、まあJavaの話はおいておいて、Javaよりも純粋さを目指しているScalaで同じようなコードを書けばコンパイラ様に怒られますな。

scala> val a1 = Array("abc")
a1: Array[java.lang.String] = Array(abc)

scala> val a2:Array[Any] = a1
// scala的にこの型の変位はダメよと怒られました
<console>:5: error: type mismatch;
 found   : Array[java.lang.String]
 required: Array[Any]
       val a2:Array[Any] = a1
                           ^

Scalaの配列は基本非変なのでコンパイルの段階で厳密にチェックします( ー`дー´)キリッとのことです。ただし、古き(Javaジェネリック導入以前の)Javaコード風の実装ができるように抜け道的に”とある型の配列”をその"スーパー型の配列"にキャストする仕組みがあるそうです。

scala> val a1 = Array("abc")
a1: Array[java.lang.String] = Array(abc)
// String型をそのスーパー型にキャストします
scala> val a2:Array[Object] = a1.asInstanceOf[Array[Object]]
a2: Array[java.lang.Object] = Array(abc)

これだとコンパイルも通るし実行できる反面、上記Javaコードと同様にArrayStoreException例外がぶん投げられる可能性があるので要注意デス(´・ω・`)

いじょー

今回も時間切れでいジョーですな。19章いつまでかかるかなぁ。。。とりあえず次回も変位指定アノテーションについてやりますよー