先ほど記述した型パラメータのサンプルでは、型パラメータ部分にどんな型でも指定できました。
しかし、型パラメータに指定する型をある程度制限したいケースも多々あると思います。そんな場合、型パラメータに制限を持たせることができます。次のような継承関係のクラス群があると仮定してください。
class Base class Ex1 extends Base class Ex2 extends Ex1
まずは「上限境界」と呼ばれる指定です。下記のように指定します。
class MySample[A <: Base]
「<:」は、Aに指定できる型はBaseクラスか、「Baseのサブクラスである」という条件になります。上記例でいうと、AにはBaseかEx1かEx2(とNothingクラス)の指定が可能です。上記クラス群をREPLで定義し、使ってみます。
scala> new MySample[Ex1] res0: MySample[Ex1] = MySample@25dad8eb scala> new MySample[String] <console>:9: error: type arguments [String] do not conform to class MySample's type parameter bounds [A <: Base]
JavaのGenericsでいう、「extends」のようなものですね。
次は「下限境界」です。これは下記のように指定します。
class MySample[A >: Ex1]
これは上限境界の逆で、型パラメータに指定できる型は、「>:」の右側で指定された型、もしくはそのスーパークラスであるという条件です。これも上記例で試してみましょう。
scala> new MySample[Ex1] res5: MySample[Ex1] = MySample@761da8fc scala> new MySample[Base] res6: MySample[Base] = MySample@7ec48b77 scala> new MySample[Ex2] <console>:11: error: type arguments [Ex2] do not conform to class MySample's type parameter bounds [A >: Ex1]
Ex1とBaseは下限境界で指定した型とそのスーパークラスなので指定可能です。しかし、Ex2は直接の継承関係にないので、エラーになります。これはJavaのGenericsでいう「super」のようなものです。
また、下限境界と上限境界を同時に指定することも可能です。下記のように指定すれば、型パラメータに指定できる型はEx1かBaseのみに限定できます(※「[A <: Base >: Ex1]」だとエラーになるので、注意)。
class MySample[A >: Ex1 <: Base]
型パラメータにクラス名ではなくクラス定義を指定する方法を紹介します。下の例のようなクラスを定義してみましょう。このクラスは「doit」という名前で引数なし、戻り値なしのメソッドを持っているクラス/トレイトであることが型パラメータに指定する方法です。
class MySample[A <: { def doit():Unit }]
条件に合うメソッドを持った「Foo」クラスを定義し、MySampleクラスをインスタンス化してみましょう。Fooクラスはdoitメソッドを持っているので、MySampleの型パラメータとして指定できます。
class Foo { def doit():Unit = println("doit!") } scala> new MySample[Foo] res22: MySample[Foo] = MySample@6d2a123d
このように、型パラメータに渡せる型は上限境界/下限境界/クラス定義と、非常に細かく制限ができます。型パラメータを使用したクラスやメソッドを定義する際には、適切な境界を設定するようにしましょう。
JavaにおけるGenericsとScalaの型パタメータ化との大きな違いは、これから紹介する変位指定の動作です。
例えば、継承関係にあるクラスParentと、そのサブクラスChildがあったとします。List[Child]型の変数はList[Parent]型の変数へ代入できるべきでしょうか。このようなケースの振る舞いを定義するのが変位指定です。
まず、下記のJavaコードを見てください。List<Object>型の変数にList<String>型の変数を代入しようとしています。これは代入しようとした時点でコンパイルエラーが発生します。
//Javaのサンプルです List<Object> objList = null; List<String> strList = new ArrayList<String>(); objList = strList; //コンパイルエラー
Scalaでは、似たようなケースがあった場合、どのようなどうになるでしょうか。次のような継承関係のクラス群があると仮定してください。
class Base class Ex extends Base class Foo[T]
そして、Foo[Base]型の引数を受け取る関数をREPLで定義します。
scala> def invariant(arg:Foo[Base]) = println("ok")
この関数に、BaseのサブクラスであるExを型パラメータに指定したFoo[Ex]を渡してみましょう。
scala> invariant(new Foo[Ex]) <console>:12: error: type mismatch; found : Foo[Ex] required: Foo[Base] Note: Ex <: Base, but class Foo is invariant in type T. You may wish to define T as +T instead. (SLS 4.5) func(new Foo[Ex])
Javaの場合と同じく、エラーになりました。Scalaの場合でも、標準ではFoo[Base]型に代入できるのはFoo[Base]型だけです。このように、何も指定しないデフォルトの型パラメータ指定を「非変」といいます。
REPLのエラーメッセージを見ると、「型パラメータでは『+T』と指定しろ」的なメッセージが出ています。
では、その通りに指定してみましょう。Fooクラスの型パラメータを指定する個所で、「+」を付けて定義します。そして、covariant関数を定義して呼び出してみます。
scala> class Foo[+T] scala> def covariant(arg:Foo[Base]) = println("ok") scala> covariant(new Foo[Ex]) ok
今度はFoo[Base]型にFoo[Ex]を渡すことができました。Foo[Base]型には、型パラメータにBasaまたはBaseのサブクラスが指定されたFoo型を代入できます。この指定を「共変」といいます。
ちなみに、ScalaのListは共変です。
そしてもう1つ、「反変」という指定もあります。これは共変の逆で、「型パラメータに指定した型のスーパークラスを指定した型」が代入可能です(※混乱しやすいので注意)。
scala> class Foo[-T] scala> def contravariant(arg:Foo[Ex]) = println("ok") scala> contravariant(new Foo[Base]) ok
Fooの型パラメータは反変として定義されているので、Foo[Base]型はFoo[Ex]型に代入可能になっています。
今回はScalaの型パラメータとその制約指定について紹介しました。境界指定と変位を組み合わせて使用することで、より柔軟な型パラメータを使った型を定義できると思います。
次回は暗黙の型変換について紹介します。
中村修太(なかむら しゅうた)
クラスメソッド勤務の新しもの好きプログラマーです。昨年、東京から山口県に引っ越し、現在はノマドワーカーとして働いています。好きなJazzを聴きながらプログラミングするのが大好きです。
Copyright © ITmedia, Inc. All Rights Reserved.