型パラメータ(type parameter)

クラスの節では触れませんでしたが、クラスは0個以上の型をパラメータとして取ることができます。これは、クラスを作る時点では何の型か特定できない場合(たとえば、コレクションクラスの要素の型)を表したい時に役に立ちます。型パラメータを入れたクラス定義の文法は次のようになります

class クラス名[型パラメータ1, 型パラメータ2, ..., 型パラメータN](コンストラクタ引数1 :コンストラクタ引数1の型, コンストラクタ引数2 :コンストラクタ引数2の型, ...)
{
  0個以上のフィールドの定義またはメソッド定義
}

型パラメータ1から型パラメータNまでは好きな名前を付け、クラス定義の中で使うことができます。とりあえず、簡単な例として、1個の要素を保持して、要素を入れる(putする)か取りだす(getする)操作ができるクラスCellを定義してみます。Cellの定義は次のようになります。

class Cell[A](var value: A) {
  def put(newValue: A): Unit = {
    value = newValue
  }

  def get(): A = value
}

これをREPLで使ってみましょう。

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

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

scala> cell.put(2)

scala> cell.get()
res1: Int = 2

scala> cell.put("something")
<console>:10: error: type mismatch;
 found   : String("something")
 required: Int
              cell.put("something")
                       ^
scala> val cell = new Cell[Int](1)
cell: Cell[Int] = Cell@79d41ed

で、型パラメータとしてInt型を与えて、その初期値として1を与えています。型パラメータにIntを与えてCellをインスタンス化したため、REPLではStringputしようとして、コンパイラにエラーとしてはじかれています。Cellは様々な型を与えてインスタンス化したいクラスであるため、クラス定義時には特定の型を与えることができません。そういった場合に、型パラメータは役に立ちます。

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

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

という選択肢しかありませんでした。しかし、前者は引数を返り値に使うという点で邪道ですし、後者の方法は多数の引数を返したい、あるいは解く問題上で意味のある名前の付けられるクラスであれば良いですが、ただ2つの値を返したいといった場合には小回りが効かず不便です。こういう場合、型パラメータを2つ取るPairクラスを作ってしまいます。Pairクラスの定義は次のようになります。toStringメソッドの定義は後で表示のために使うだけなので気にしないでください。

class Pair[A, B](val a: A, val b: B) {
  override def toString(): String = "(" + a + "," + b + ")"
}

このクラスPairの利用法としては、たとえば割り算の商と余りの両方を返すメソッドdivideが挙げられます。divideの定義は次のようになります。

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

これらをREPLにまとめて流し込むと次のようになります。

scala> class Pair[A, B](val a: A, val b: B) {
     |   override def toString(): String = "(" + a + "," + b + ")"
     | }
defined class Pair

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(7, 3)
res0: Pair[Int,Int] = (2,1)

7割る3の商と余りがres0に入っていることがわかります。なお、ここではnew Pair[Int, Int](m / n, m % n)としましたが、引数の型から型パラメータの型を推測できる場合、省略できます。この場合、Pairのコンストラクタに与える引数はIntIntなので、new Pair(m / n, m % n)としても同じ意味になります。このPairは2つの異なる型(同じ型でも良い)を返り値として返したい全ての場合に使うことができます。このように、どの型でも同じ処理を行う場合を抽象化できるのが型パラメータの利点です。

ちなみに、このPairのようなクラスはScalaではよく使われるため、Tuple1からTuple22(Tupleの後の数字は要素数)があらかじめ用意されています。また、インスタンス化する際も、

scala> val m = 7
m: Int = 7

scala> val n = 3
n: Int = 3

scala> new Tuple2(m / n, m % n)
res1: (Int, Int) = (2,1)

などとしなくても、

scala> val m = 7
m: Int = 7

scala> val n = 3
n: Int = 3

scala> (m / n, m % n)
res2: (Int, Int) = (2,1)

とすれば良いようになっています。

変位指定(variance)

この節では、型パラメータに関する性質である反変、共変について学びます。

共変(covariant)

Scalaでは、何も指定しなかった型パラメータは通常は非変(invariant)になります。非変というのは、型パラメータを持ったクラスG、型パラメータABがあったとき、A = Bのときにのみ

val : G[A] = G[B]

というような代入が許されるという性質を表します。これは、違う型パラメータを与えたクラスは違う型になることを考えれば自然な性質です。ここであえて非変について言及したのは、Javaの組み込み配列クラスは標準で非変ではなく共変であるという設計ミスを犯しているからです。

ここでまだ共変について言及していなかったので、簡単に定義を示しましょう。共変というのは、型パラメータを持ったクラスG、型パラメータABがあったとき、AB を継承しているときにのみ、

val : G[B] = G[A]

というような代入が許される性質を表します。Scalaでは、クラス定義時に

class G[+A]

のように型パラメータの前に+を付けるとその型パラメータは(あるいはそのクラスは)共変になります。

このままだと定義が抽象的でわかりづらいかもしれないので、具体的な例として配列型を挙げて説明します。配列型はJavaでは共変なのに対してScalaでは非変であるという点において、面白い例です。まずはJavaの例です。G = 配列、 A = String, B = Objectとして読んでください。

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

このコード断片はJavaのコードとしてはコンパイルを通ります。ぱっと見でも、Objectの配列を表す変数にStringの配列を渡すことができるのは理にかなっているように思えます。しかし、このコードを実行すると例外 java.lang.ArrayStoreException が発生します。これは、objectsに入っているのが実際にはStringの配列(Stringのみを要素として持つ)なのに、2行目でint型(ボクシング変換されてInteger型)の値である100を渡そうとしていることによります。

一方、Scalaでは同様のコードの一行目に相当するコードをコンパイルしようとした時点で、次のようなコンパイルエラーが出ます(Anyは全ての型のスーパークラスで、AnyRefに加え、AnyVal(値型)の値も格納できます)。

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

このような結果になるのは、Scalaでは配列は非変だからです。静的型付き言語の型安全性とは、コンパイル時により多くのプログラミングエラーを捕捉するものであるとするなら、配列の設計はScalaの方がJavaより型安全であると言えます。

さて、Scalaでは型パラメータを共変にした時点で、安全ではない操作はコンパイラがエラーを出してくれるので安心ですが、共変をどのような場合に使えるかを知っておくのは意味があります。たとえば、先ほど作成したクラスPair[A, B]について考えてみましょう。Pair[A, B]は一度インスタンス化したら、変更する操作ができませんから、ArrayStoreExceptionのような例外は起こり得ません。実際、Pair[A, B]は安全に共変にできるクラスで、class Pair[+A, +B]のようにしても問題が起きません。

scala> class Pair[+A, +B](val a: A, val b: B) {
     |   override def toString(): String = "(" + a + "," + b + ")"
     | }
defined class Pair

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

ここで、Pairは作成時に値を与えたら後は変更できず、したがってArrayStoreExceptionのような例外が発生する余地がないことがわかります。一般的には、一度作成したら変更できない(immutable)などの型パラメータは共変にしても多くの場合問題がありません。

演習問題

次のimmutableStack型の定義(途中)があります。???の箇所を埋めて、Stackの定義を完成させなさい。なお、E >: Aは、EAの継承元である、という制約を表しています。

trait Stack[+A] {
  def push[E >: A](e: E): Stack[E]
  def top: A
  def pop: Stack[A]
  def isEmpty: Boolean
}

class NonEmptyStack[+A](private val first: A, private val rest: Stack[A]) extends Stack[A] {
  def push[E >: A](e: E): Stack[E] = ???
  def top: A = ???
  def pop: Stack[A] = ???
  def isEmpty: Boolean = ???
}

case object EmptyStack extends Stack[Nothing] {
  def push[E >: Nothing](e: E): Stack[E] = new NonEmptyStack[E](e, this)
  def top: Nothing = throw new IllegalArgumentException("empty stack")
  def pop: Nothing = throw new IllegalArgumentException("empty stack")
  def isEmpty: Boolean = true
}

object Stack {
  def apply(): Stack[Nothing] = EmptyStack
}

また、Nothingは全ての型のサブクラスであるような型を表現します。Stack[A]は共変なので、Stack[Nothing]はどんな型のStack変数にでも格納することができます。例えばStack[Nothing]型であるEmptyStackは、Stack[Int]型の変数とStack[String]型の変数の両方に代入することができます。

scala> val intStack: Stack[Int] = Stack()
intStack: Stack[Int] = EmptyStack

scala> val stringStack: Stack[String] = Stack()
stringStack: Stack[String] = EmptyStack

反変(contravariant)

次は共変とちょうど対になる性質である反変です。簡単に定義を示しましょう。反変というのは、型パラメータを持ったクラスG、型パラメータABがあったとき、AB を継承しているときにのみ、

val : G[A] = G[B]

というような代入が許される性質を表します。Scalaでは、クラス定義時に

class G[-A]

のように型パラメータの前に-を付けるとその型パラメータは(あるいはそのクラスは)反変になります。

反変の例として最もわかりやすいものの1つが関数の型です。たとえば、型ABがあったとき、

val x1: A => AnyRef = B => AnyRef型の値
x1(A型の値)

というプログラムの断片が成功するためには、ABを継承する必要があります。その逆では駄目です。仮に、A = String, B = AnyRef として考えてみましょう。

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

ここでx1に実際に入っているのはAnyRef => AnyRef型の値であるため、引数としてString型の値を与えても、AnyRef型の引数にString型の値を与えるのと同様であり、問題なく成功します。ABが逆で、A = AnyRef, B = Stringの場合、String型の引数にAnyRef型の値を与えるのと同様になってしまうので、これはx1へ値を代入する時点でコンパイルエラーになるべきであり、実際にコンパイルエラーになります。

実際にREPLで試してみましょう。

scala> val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
<console>:7: error: type mismatch;
 found   : String => AnyRef
 required: AnyRef => AnyRef
       val x1: AnyRef => AnyRef = (x: String) => (x:AnyRef)
                                              ^

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

このように、先ほど述べたような結果になっています。

型パラメータの境界(bounds)

型パラメータTに対して何も指定しない場合、その型パラメータTは、どんな型でも入り得ることしかわかりません。そのため、何も指定しない型パラメータTに対して呼び出せるメソッドはAnyに対するもののみになります。しかし、たとえば、順序がある要素からなるリストをソートしたい場合など、Tに対して制約を書けると便利な場合があります。そのような場合に使えるのが、型パラメータの境界(bounds)です。型パラメータの境界には2種類あります。

上限境界(upper bounds)

1つ目は、型パラメータがどのような型を継承しているかを指定する上限境界(upper bounds)です。上限境界では、型パラメータの後に、<:を記述し、それに続いて制約となる型を記述します。以下では、showによって文字列化できるクラスShowを定義したうえで、Showであるような型のみを要素として持つShowablePairを定義しています。

abstract class Show {
  def show: String
}
class ShowablePair[A <: Show, B <: Show](val a: A, val b: B) extends Show {
  override def show: String = "(" + a.show + "," + b.show + ")"
}

ここで、型パラメータABともに上限境界としてShowが指定されているため、abに対してshowを呼び出すことができます。なお、上限境界を明示的に指定しなかった場合、Anyが指定されたものとみなされます。

下限境界(lower bounds)

2つ目は、型パラメータがどのような型のスーパータイプであるかを指定する下限境界(lower bounds)です。下限境界は、共変パラメータと共に用いることが多い機能です。実際に例を見ます。

まず、共変の練習問題であったような、イミュータブルなStackクラスを定義します。このStackは共変にしたいとします。

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

しかし、この定義は、以下のようなコンパイルエラーになります。

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

このコンパイルエラーは、共変な型パラメータAが反変な位置(反変な型パラメータが出現できる箇所)に出現したということを言っています。一般に、引数の位置に共変型パラメータEの値が来た場合、型安全性が壊れる可能性があるため、このようなエラーが出ます。しかし、このStackは配列と違ってイミュータブルであるため、本来ならば型安全性上の問題は起きません。この問題に対処するために型パラメータの下限境界を使うことができます。型パラメータEpushに追加し、その下限境界として、Stack の型パラメータAを指定します。

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

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

results matching ""

    No results matching ""