JavaのGenericsよりも便利なScalaの型パラメータスケーラブルで関数型でオブジェクト指向なScala入門(8)(2/2 ページ)

» 2012年08月09日 00時00分 公開
[中村修太クラスメソッド株式会社]
前のページへ 1|2       

上限境界/下限境界とパラメータ制約

 先ほど記述した型パラメータのサンプルでは、型パラメータ部分にどんな型でも指定できました。

 しかし、型パラメータに指定する型をある程度制限したいケースも多々あると思います。そんな場合、型パラメータに制限を持たせることができます。次のような継承関係のクラス群があると仮定してください。

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との大きな違い「変位指定」

 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を聴きながらプログラミングするのが大好きです。



前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。