こまぶろ

技術のこととか仕事のこととか。

ScalaとJavaにおける変位指定と型パラメータの境界

Scalaのジェネリクスの勉強で、簡単なリストを実装してみよう!というものが出てきた。

詳細は省くが、リストのadd()メソッドは以下のように宣言される。

/* Scala */
abstract class SimpleList[+A] {
  def add[B >: A](element: B): SimpleList[B]
}

ここで、クラスの型の変位の指定として+A、つまり共変(covariant)であることが指定されている。また、add()メソッドでは、境界型パラメータによって対象の型(A)およびそのスーパークラス(B)を引数に取り、また戻り値はその型のリストとなることが指定されている。

本記事では、変位の指定と境界型パラメータについてScalaとJavaを往復しながら見ていく。

ジェネリクスの変位

変位を指定できるのは、Javaのジェネリクスにはない機能だ。Javaのジェネリクスは不変(invariant)となっており、以下のような代入は許されない。いずれもコンパイルエラーとなる。

/* Java */
List<Number> listOfSuper = new ArrayList<Integer>(); 
List<Integer> listOfSub = new ArrayList<Number>();

これに対して、Scalaで変位(上の例の場合は共変)を指定した場合には、以下のような代入が可能になる。CatAnimalのサブクラス)をパラメータにとったSimpleList[Cat]を、AnimalをパラメータにとったSimpleList[Animal]に代入できる。

/* Scala */
class Animal(val age: Int)
class Cat(age: Int) extends Animal(age)
val listOfSuper: SimpleList[Animal] = new SimpleList[Cat](/* 省略 */)

一方、反変(-A)を指定した場合は、型パラメータに代入された型の継承関係とは反対の関係になる。つまり、スーパークラスを型パラメータにとった値を、サブクラスを型パラメータにとった変数に代入することができる(=サブクラスの値をスーパークラスの変数に代入できるのと反対)。また、不変(A)を指定した場合には、Javaにおけるジェネリクスと同様、型パラメータ同士の継承関係にかかわらず、別の型として扱われる。

Use-site variance と Declaration-site variance、境界ワイルドカード型

Javaのジェネリクスは不変だが、境界ワイルドカード型を利用することで、部分的に共変や反変類似の効果を起こすことができる。以下の例では、メソッド定義時に下限境界<? super A>を利用している。

/* Java */
abstract class SimpleList<A> {
  abstract SimpleList<A> filter(Predicate<? super A> predicate);
}

上記のように定義されたメソッドを実際に呼び出すとき、引数predicateにはAまたはそのスーパークラスを型パラメータにとったPredicateのインスタンスを渡すことができる。再び、IntegerとNumberを例に示す。

/* Java */
SImpleList<Integer> list.= new Cons<>(1, new Cons<>(2, new Cons<>(3, new Empty<>()));
SimpleList<Integer> filtered = list.filter(
    new Predicate<Number> {
      @Override
      public boolean test(Number t) {
        return t.intValue() % 2 == 1;
      }
    });

上の例では、Integer型のリストのメソッドfilter(Predicate<? super A> predicate)に対して、Predicate<Number>*1のインスタンスを渡している。これは、Predicate<Number>Predicate<Integer>として扱われうる(=反変)ということではなく、あくまで引数で受け取る側がいずれの型も受け入れているに過ぎない。このように、使用する箇所において変位を指定する方式を、Use-site varianceという。

これに対して、Scalaが採用している方式は、Declaration-site varianceといい、宣言する箇所において変位を指定する。以下のサンプルコードにおけるPredicateでは、宣言時に変位を反変(-T)と指定している。宣言時に指定しているので、使用する箇所の方では単にdef filter(predicate: Predicate[A]): SimpleList[A]とすればよい。

/* Scala */
trait Predicate[-T] {
  def test(element: T): Boolean
}

abstract class SimpleList[+A] {
  def filter(predicate: Predicate[A]): SimpleList[A]
}

Predicateは反変に指定しているので、Predicate[Animal]Predicate[Cat]型の変数に代入できる。以下の例では、このPredicateの性質を利用して、SimpleList[Cat]filter(predicate: Predicate[Cat])メソッドに対して、Predicate[Animal]型のyoungerThan10を渡している。

/* Scala */
val listOfCat: SimpleList[Cat] = Cons(new Cat(4), Cons(new Cat(7), Cons(new Cat(12), Empty)))
val youngerThan10: Predicate[Animal] = new Predicate[Animal] {
  override def test(t: Animal): Boolean = t.age < 10
}
listOfCat.filter(youngerThan10) // Predicate[Animal]はPredicate[Cat]に代入できる(反変)

反変(あるいは下限境界ワイルドカード型)は、共変に比べて直感的にはわかりづらいが、具体的に考えるとわかりやすい。上の例で、filter()メソッドが引数であるPredicate型のオブジェクトに期待しているのは、thisの要素(Catまたはそのサブクラス*2のオブジェクト)についてBooleanの値を戻してくれることだ。個々の要素は、Predicate[MyClass]test(element: MyClass): Booleanに渡される引数になるから、このMyClassCatクラスを代入可能な型であれば、filter()メソッドに渡されるべきPredicateの型パラメータにとって適格だということになる。そのような性質を満たすMyClassは、Cat自身あるいはそのスーパークラスである。

関連して、『Effective Java』でも紹介されているJavaのジェネリクスについての原則で、「PECS(Producer Extends, Consumer Super)」というものがある。PECSは、コレクションを引数に取るメソッドの定義の際に、当該コレクションが要素を供給する側(Producer)であるときにはCollection(? extends MyClass)を、要素を受容する側(Consumer)であるときにはCollection(? super MyClass)を利用せよ、と指示する。要素を受容する側は、自らのメソッド(たとえばadd())の引数に要素を受け取るので、上の例におけるPredicateと同じ状況になる。

反対に、要素を供給する側は、自らのメソッド(たとえばget())の戻り値に要素を与える。この場合、戻り値を受け取る側の型がCatであるとすれば、Cat型の変数に代入できる型を返せればよく、それはつまりCat自身あるいはそのサブクラスだ。それを境界ワイルドカード型で表現すると、<? extends Cat>となる。

型パラメータの境界

境界ワイルドカード型と混同しやすいのが、境界型パラメータである。Javaにおいて、境界型パラメータは、以下のようなものである。

/* Java */
<E extends Number> // 上限境界型パラメータ
// <E super Integer>   // これは存在しない!(下限境界型パラメータ)

境界ワイルドカード型が、どのような型のインスタンスを受け取ることができるかを指定する(Predicate<? extends Animal>には、Predicate<Cat>を代入できる)のに対し、境界型パラメータは、その型パラメータに指定することのできる型を指定する。以下のチュートリアル記載の例では、型パラメータTComparable<T>の実装クラスに制限している。

/* Java */
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
    int count = 0;
    for (T e : anArray)
        if (e.compareTo(elem) > 0)
            ++count;
    return count;
}

境界ワイルドカード型との大きな違いは、Tを引数の型の指定に利用できるという点だ(だから「パラメータ」なのだ)。Javaの場合は、<? extends MyClass><T extends MyClass>と似た構文になっているため非常にわかりづらい(しかもいずれも使用するときに記述する)のに対し、Scalaでは明快に異なる構文になる。冒頭に掲げたadd()メソッドで利用されている[B >: A]がScalaにおける境界型パラメータだ。

/* 再掲 */
/* Scala */
abstract class SimpleList[+A] {
  def add[B >: A](element: B): SimpleList[B]
}

上のadd()メソッドでは、元々のリストの型パラメータであるA自身あるいはそのスーパークラスを型パラメータBとして利用できるようになっている。これにより、たとえばCatAとした場合のSimpleList[Cat]は、add[Animal](element: Animal): SimpleList[Animal]というメソッドを持っているのと同様の機能を持つ。このメソッドは、猫のリストに(必ずしも猫ではない)動物を追加することができ、追加後の新しいリストは動物のリストとなる、ということを意味している。

先述の通り、Javaにおいて境界型パラメータは上限(extends)のみ存在しており、下限(super)は存在しない。それゆえ、上のScalaのコードに対応する以下のようなコードをJavaで書くことはできない。

/* Java の擬似コード */
abstract class SimpleList<A> {
  // これはコンパイルエラーになる
  abstract SimpleList<B super A> add(B element);
}

下限境界型パラメータがJavaに存在しない理由は、調べてみたがよくわからなかった。

docs.oracle.com

おわりに

以上、ScalaとJavaにおける変位指定と型パラメータの境界について見てきた。今回、Scalaを勉強したことを通じて、曖昧な理解しかしていなかったJavaのジェネリクスについて学びなおすことができた。また、Scalaの下限境界型パラメータを利用したadd()は提示されてみると実に自然な設計で、言語仕様上の実装の制限が設計の考慮の幅を狭めてしまっているということを確認できたいい体験だった。

*1:正確にはそれをもとに作られた匿名クラス。

*2:上の例ではサブクラスは作成していないが。