8.1 型パラメータの境界

クラスをジェネリックにする方法を知った今では、前に書いたクラスの中には一般化した方が自然なものがあります。たとえばクラス IntSet は、任意の要素型を持てるように一般化できます。やってみましょう。ジェネリックな集合の抽象クラスは簡単に書けます。

abstract class Set[A] { def incl(x: A): Set[A] def contains(x: A): Boolean }

しかしもし二分木として実装し続けたいなら、問題に直面します。メソッド contains と incl はどちらも、要素をメソッド < と > を使って比較します。IntSet については、これは OK です。なぜなら Int はそれら2つのメソッドを持っているからです。しかし任意の型パラメータ a に対しては、それは保証できません。したがって先の実装、たとえば contains ではコンパイルエラーが生じるでしょう。

def contains(x: Int): Boolean = if (x < elem) left contains x ^ < not a member of type A.

問題を解決する一つの方法は、型 A と置き換え可能な正当な型を、正しい型のメソッド < と > を持つ型だけに制限することです。Scala 標準ライブラリには、型 A の値と (< と > によって) 比較可能な値を表現する、トレイト Ordered[A] があります。このトレイトは次のように定義されています。

/** データを完全に順序づけるためのクラス */ trait Ordered[A] { /** this をオペランド that と比較した結果。 * returns 'x' where * x < 0 iff this < that * x == 0 iff this == that * x > 0 iff this > that */ def compare(that: A): Int def < (that: A): Boolean = (this compare that) < 0 def > (that: A): Boolean = (this compare that) > 0 def <= (that: A): Boolean = (this compare that) <= 0 def >= (that: A): Boolean = (this compare that) >= 0 def compareTo(that: A): Int = compare(that) }

型が Ordered のサブタイプであることを要求することで、互換性を強制できます。これは Set の型パラメータに上界境界(upper bound)を与えることでなされます。

trait Set[A <: Ordered[A]] { def incl(x: A): Set[A] def contains(x: A): Boolean }

パラメータ宣言 A <: Ordered[A] は型パラメータとしての A を、A は Ordered[A] のサブタイプでなくてはならない、として導入します。すなわち、その値は同じ型の値と比較可能でなくてはなりません。

この制限によって、ジェネリックな集合の抽象化を、前の IntSet の場合と同じように実装できます。

class EmptySet[A <: Ordered[A]] extends Set[A] { def contains(x: A): Boolean = false def incl(x: A): Set[A] = new NonEmptySet(x, new EmptySet[A], new EmptySet[A]) } class NonEmptySet[A <: Ordered[A]] (elem: A, left: Set[A], right: Set[A]) extends Set[A] { def contains(x: A): Boolean = if (x < elem) left contains x else if (x > elem) right contains x else true def incl(x: A): Set[A] = if (x < elem) new NonEmptySet(elem, left incl x, right) else if (x > elem) new NonEmptySet(elem, left, right incl x) else this }

オブジェクト生成 new NonEmptySet(...) において、型引数を書いていないことに注意して下さい。多相的メソッドと同様に、コンストラクタ呼び出しで型引数が書かれていない時、それは値引数と期待される結果型(戻り値型)から推論されます。

以下はジェネリックな集合の抽象化を使う例です。まづ、Ordered のサブクラスを次のように作りましょう。

case class Num(value: Double) extends Ordered[Num] { def compare(that: Num): Int = if (this.value < that.value) -1 else if (this.value > that.value) 1 else 0 }

すると

val s = new EmptySet[Num].incl(Num(1.0)).incl(Num(2.0)) s.contains(Num(1.5))

これは OK です。なぜなら型 Num はトレイト Ordered[Num] を実装しているからです。しかし次の例はエラーになります。

val s = new EmptySet[java.io.File] ^ java.io.File does not conform to type parameter bound Ordered[java.io.File].

型パラメータ境界の問題は、前もって考えておくべきことです。もし Num を Ordered のサブクラスとして宣言していなければ、Num を集合の要素として使用できません。同様に、Java から継承した Int、Double、String といった型は Ordered のサブクラスではないので、それらの型の値は集合の要素として使用できません。

それらの型を要素として許すもっと柔軟なデザインがあります。 可視境界 (view bound)を今までに見てきた単純な型境界の代わりに使うことです。先の例でこれによって起こる変更は、単に型パラメータが次のようになることです。

trait Set[A <% Ordered[A]] ... class EmptySet[A <% Ordered[A]] ... class NonEmptySet[A <% Ordered[A]] ...

可視境界 <% は通常の上限境界 <: より弱いです。可視境界をもつ型パラメータ節 [A <% T] は単に、境界づけられる型 A が境界となる型 T へ、暗黙の型変換を使って 変換可能 なことだけを指定します。

Scala ライブラリは、プリミティブ型や String を含む数多くの型について、暗黙の型変換を事前に定義しています。したがって、再デザインされた集合の抽象化は、これらの型についてもインスタンス化できます。暗黙の変換と可視境界に関する更なる説明は第 15 章にあります。