読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

参考文献

dwango.github.io

10. 型パラメータと変位指定

型パラメータ

クラスは0個以上の型をパラメータとして取ることができる。
これは,クラスを作る時点では何の型か特定できない場合を表したいときに役立つ。

class クラス名[型パラメータ1, ... 型パラメータN](コンストラクタ引数: コンストラクタ引数の型, ...)
{
  // クラス定義
}

型パラメータ1からNまでは好きな名前を付け,クラス定義の中で使うことができる。
簡単な例として,1個の要素を保持して要素をput/get操作ができるCellクラスを定義してみる。

scala> class Cell[T](var value: T) {
     |   def put(newValue: T): Unit = {
     |     value = newValue
     |   }
     | 
     |   def get(): T = value
     | }
defined class Cell

scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@1ad78261

scala> cell.get
res47: Int = 1

scala> cell.put(2)

scala> cell.get
res49: Int = 2

new Cell[Int](1)で,型パラメータとしてInt型を与えて,その初期値として1を与えている。
Cellのように様々な型を与えてインスタンス化したい場合,クラス定義時には特定の型を与えることができないので,型パラメータが役に立ちます。

もう少し実用的な例を見てみる。
メソッドから複数の値を返したいという要求はプログラミングでよく発生するが,そのような場合に型パラメータが無い言語では,

  • 片方を返り値として,もう片方を引数経由で返す
  • 複数の返り値専用クラスを必要になる度に作る

という選択肢しかなく,前者は引数を返り値に使うという点で邪道であり,後者はただ2つの値を返したいといったような場合には小回りが効かず不便である。
こういう場合,型パラメータを二つとるPairクラスを作ってしまえばよい。

scala> class Pair[T1, T2](val t1: T1, val t2: T2) {
     |   override def toString(): String = "(" + t1 + "," + t2 + ")"
     | }
defined class Pair

このPairクラスを利用する例として,割り算の商と余りの両方を返すdevideメソッドを作ってみる。

scala> def divide(m: Int, n: Int): Pair[Int, Int] = new Pair[Int, Int](m / n, m % n)
divide: (m: Int, n: Int)Pair[Int,Int]

scala> divide(9, 2)
res51: Pair[Int,Int] = (4,1)

このPairは2つの型を返り値として返したい全ての場合に使うことができる。どの型でも同じ処理を行う場合を抽象化できるのが型パラメータの利点になる。
なお,引数の型から型パラメータの型を推測できる場合省略できる。よって,new Pair(m / n, m % n)と書いても同じ意味になる。

ちなみに,PairのようなクラスはScalaではTuple1からTuple22としてあらかじめ用意されており,()で括ることでインスタンス化できるようになっている。

scala> new Tuple2(7 / 3, 7 % 3)
res52: (Int, Int) = (2,1)

scala> (7 / 3, 7 % 3)
res53: (Int, Int) = (2,1)

変位指定

共変

Scalaでは,何も指定しなかった型パラメータは通常は非変になる。
非変というのは,型パラメータT1とT2が等しいときのみ,G[T1] = G[T2]というような代入が許されるという性質を表す。

一方,共変はT1がT2を継承しているときのみ,G[T2] = G[T1]というような代入が許される性質を表す。Scalaでは,クラス定義時に

class G[+T]

のように型パラメータの前に+を付けるとその型パラメータは共変になる。

具体的な例として,配列型を挙げてみる。配列型はJavaでは共変なのに対し,Scalaでは非変であるという違いがある。

Object[] objects = new String[1];
objects[0] = 100;

上記のコードは,Javaのコードとしてはコンパイルを通る。Objectの配列を表す変数にStringの配列を渡すことができるのは理にかなっているようにも思えるが,このコードを実行するとArayStoreExceptionが発生してしまう。
これは,objectsに入っているのがStringの配列なのにも関わらず,2行目でint型の100を渡そうとしていることによるものである。

一方,Scalaでは同様のコードの一行目に相当するコードをコンパイルしようとした時点でコンパイルエラーが出る。

scala> val arr: Array[Any] = new Array[String](1)
<console>:11: error: type mismatch;
 found   : Array[String]
 required: Array[Any]

これは,Scalaでは配列は非変なためである。コンパイル時により多くのプログラミングエラーを補足する方が型安全であるとすれば,配列の設計はScalaの方がJavaより型安全であるといえる。

Scalaでは型パラメータを共変にした時点で安全ではない操作はコンパイルエラーが出るので安心だが,共変をどのような場合に使えるかを知っておくのには意味がある。
例えば,Pair[T1, T2]は一度インスタンス化したら変更する操作ができないので,安全に共変にできるクラスである。

scala> class Pair[+T1, +T2](val t1: T1, val t2: T2) {
     |   override def toString(): String = "(" + t1 + "," + t2 + ")"
     | }
defined class Pair

scala> val pair: Pair[AnyRef, AnyRef] = new Pair[String, String]("foo", "bar")
pair: Pair[AnyRef,AnyRef] = (foo,bar)

Pairは作成時に値を与えたら後は変更できないので,ArrayStoreExceptionのような例外が発生する余地がないことがわかる。
一般的には,一度作成したら変更できないイミュータブルな型パラメータなどは共変にしても多くの場合問題がない。

反変

反変は共変とちょうど対になる概念で,T1がT2を継承しているときのみG[T1] = G[T2]という代入が許される性質を表す。
Scalaではクラス定義時に型パラメータの前に-を付ける。

class G[-T]

反変の例として最もわかりやすいのは関数の型である。

val x1: T1 => AnyRef = T2 => AnyRef型の値

というプログラムが成功するためには,T1がT2を継承している必要がある。
仮にT1 = String, T2 = AnyRefとすると,

val x1: String => AnyRef = AnyRef => AnyRef型の値

ここでx1に実際に入っているのはAnyRef => AnyRef型の値であるため,引数としてString型の値を与えても,AnyRef型の引数にString型の値を与えるのと同様,問題なく成功する。

scala> val x1: String => AnyRef = (x: AnyRef) => x
x1: String => AnyRef = <function1>

型パラメータの境界

型パラメータTに対して何も指定しない場合,その型パラメータTはどんな型でも入り得ることしかわからない。
そのため,何も指定しない型パラメータに対して呼び出せるメソッドはAnyに対するもののみとなる。
しかし,順序がある要素からなるリストをソートしたい場合など,Tに対して制約をかけると便利な場合がある。

上限境界

1つ目は,型パラメータがどのような型を継承しているかを指定する上限境界である。
型パラメータの後に

scala> abstract class Show {
     |   def show: String
     | }
defined class Show

scala> class ShowablePair[T1 <: Show, T2 <: Show](val t1: T1, val t2: T2) extends Show {
     |   override def show: String = "(" + t1.show + "," + t2.show + ")"
     | }
defined class ShowablePair

型パラメータT1, T2ともに上限境界としてShowが指定されているため,t1とt2に対してshowを呼び出すことができる。

scala> class Test extends Show {
     |   def show: String = "hoge"
     | }
defined class Test

scala> class Test2 extends Show {
     |   def show: String = "hoge2"
     | }
defined class Test2

scala> val sp = new ShowablePair(new Test, new Test2)
sp: ShowablePair[Test,Test2] = ShowablePair@6dc957cc

scala> sp.show
res16: String = (hoge,hoge2)
下限境界

2つ目は,型パラメータがどのような型のスーパータイプであるかを指定する下限境界である。
下限境界は,共変パラメータとともに用いることが多い。

abstract class Stack[+E]{
  def push(element: E): Stack[E]
  def top: E
  def pop: Stack[E]
  def isEmpty: Boolean
}

上記はイミュータブルなStackクラスの例で,共変にしている。
しかし,この定義は以下のようなコンパイルエラーになる。

error: covariant type E occurs in contravariant position in type E of value element
         def push(element: E): Stack[E]
                           ^

このエラーは,共変な型パラメータEが,反変な型パラメータが出現できる箇所に出現したというエラーになる。
一般に,引数の位置に共変型パラメータが来た場合,型安全性が壊れる可能性があるため,このようなエラーになる。
この問題に対処するには,型パラメータFをpushに追加し,その下限境界としてStackの型パラメータEを指定する。

  def push[F >: E](element: F): Stack[F]

これにより,コンパイラはStackにはEの任意のスーパータイプの値が入れられる可能性があることがわかる。
また,型パラメータFは共変ではないので,引数に出現してもエラーにならない。
このようにして,下限境界を利用して型安全なStackと共変性を両立することができる。