Locked History Actions

Diff for "scala/typeSystem"

Differences between revisions 13 and 14
Deletions are marked like this. Additions are marked like this.
Line 187: Line 187:
※コンパイラがどのような判断をするかは、日本語訳「Scalaスケーラブルプログラミング」の360ページあたりに解説があるのだが、この説明はちんぷんかんぷんである(原書を読んでいないので、訳が間違っている可能性もあり)。現時点では「共変的パラメータはメソッド引数には使えない」とだけ覚えておく。 ※コンパイラがどのような判断をするかは、日本語訳「Scalaスケーラブルプログラミング」の360ページあたりに解説があるのだが、この説明はちんぷんかんぷんである。現時点では「共変的パラメータはメソッド引数には使えない」とだけ覚えておく。
Line 189: Line 189:

Scalaの型システム

単純な型パラメータ

Javaでは以下のように書ける。

class Animal { }
class Fish extends Animal { }
class Zoo<T> {
    public void add(T animal) {
    }
}
...
        Zoo<Animal>zoo = new Zoo<Animal>();
        zoo.add(new Fish());
...

これと等価な(たぶん)Scalaコードは以下。

class Animal
class Fish extends Animal;
class Zoo[T] {
  def add(a:T) { }
}

val zoo = new Zoo[Animal];
zoo add new Fish

上限型境界

上のコードのままだと、Animal以外のZooの作成が可能になってしまう。

  Zoo<Integer>zooInt = new Zoo<Integer>(); // 可能だが意味不明

また、そもそもこのままではZooクラスの中でAnimalのメソッドを呼び出せない。Tはどんな型でも可能だからである。 Zooの中でAnimalのメソッドを(キャスト無しに)呼び出すためには、TがAnimalクラスあるいはそのサブクラスであることを明示する必要がある。

そこで上限型境界指定を行う。Javaでは以下のようなものだった

class Zoo<T extends Animal> {
...
}
Zoo<Fish>fishZoo = new Zoo<Fish>(); // OK
Zoo<Integer>intZoo = new Zoo<Integer>(); // エラー

Scalaでは以下になる

class Zoo[T <: Animal] {
...
}
....
val fishZoo = new Zoo[Fish]; // OK
val intZoo = new Zoo[Int]; // エラー

下限型境界

上限型境界と同様に、下限型境界というものもある。Scalaでは

class Zoo[T >: Animal] {

などと記述するが、Javaにはこの機能がない。つまり、

class Zoo<T super Animal> {

と書くことはできない。後述する変位指定ではJavaも

<? super Animal>

などと書けるのだが(だったかな?)、下限型境界指定は存在しない。

しかし、そもそも下限型境界の使いどころが不明なのでこれについては省略。

変位指定の書き方

「変位」とは何か、JavaとScalaのその違いは何かはおいておき、ここではその書き方のみに注目してみる。

Javaの変位指定

Javaでは「その型を使う時に」「? extends Animal」などと記述して変位を指定する。 たとえば、

class Animal { }
class Fish extends Animal { }
class Zoo<T extends Animal> {
...
}
...
        Zoo<Fish>fishZoo = new Zoo<Fish>();
        Zoo<Animal>animalZoo1 = fishZoo; // エラー
        Zoo<? extends Animal>animalZoo2 = fishZoo; // ※OK
...

上の例でfishZooはanimalZoo1に代入することはできない(コンパイルエラー)。animalZoo2では、<? extends Animal>として変位を指定している。

Scalaの変位指定

ScalaではJavaのやり方とはまったく異なる。Javaでは上のように、「呼び出しサイト」で定義するが、Scalaでは「宣言サイト」で定義する(これらの言葉は、書籍「プログラミングScala」から拝借)。

以下はだめだが、

class Animal
class Fish extends Animal;
class Zoo[T <: Animal] {
}
val fishZoo = new Zoo[Fish];
val animalZoo: Zoo[Animal] = fishZoo;

以下はOK

class Animal
class Fish extends Animal;
class Zoo[+T <: Animal] { // Tの前に+をつけただけ
}
val fishZoo = new Zoo[Fish];
val animalZoo: Zoo[Animal] = fishZoo;

上限型境界と変位指定の記法

Javaでは、上限型境界も変位指定も、同じような「何々 extends 何々」という記法を用いるのだが、これらの機能はまったく異なるため、混乱の元となっている(と思われる、私も混乱した)。Scalaではこれを別の記法にして、明確に区別している。

さらに、JavaとScalaでの変位指定を行う場所に注目しなければならない。Javaでは、そのクラスを「使う側」が「使うたび」に指定しなければならないのだが、Scalaでは「定義する側」が一度だけ指定すればよい。この方が使う側にとっては楽だである。

変位とは何か

それでは「変位」とはいったい何だろうか。よく出される例は、Javaの配列である(書籍「Scalaスケーラブルプログラミング」でもこの例が使われている)。

Javaの配列は通常のオブジェクトとは異なるのだが、ここでは仮に、型パラメータ指定の可能な通常のオブジェクトと考えてみる。 型パラメータを指定して配列を作成し、その要素を設定するにはこうする。

  String[]a = new String[100];
  a[0] = "abc";
  a[1] = "xyz";

配列には、そもそもObject以下のすべてのクラスのオブジェクトが格納できるのだが、ここでは型パラメータとしてStringを指定したため、この配列にはStringしか格納できない。 ところが、配列を操作するユーティリティはそもそもObjectの配列を対象としている。たとえば、 System.arraycopyの定義は、

arraycopy(Object[]src, int srcPos, Object[]dest, int destPos, int length) 

となっており、当然型が異なる。

もし型を厳格に守るならString配列をObject配列として扱うことはできない。その一方で、あらゆる型の配列について、それぞれのarraycopyを記述するのは無駄である。このため、Javaの設計では、配列に対して何の指定もなしに「変位」できるようにしてしまった(この場合は「共変」というらしい)。

ところが、その一方で、この便利な機能によっておかしなことも起こりかねない。 例えば、次のようなメソッドをString配列に対して適用すると、

public static void badMethod(Object[]array) {
  array[0] = new Integer(0);
}

RuntimeExceptionが発生する。配列の要素はStringであるはずなのに、Integerを代入しようとしたからだ。

変位の必要性と難しさ

変位(この場合は「共変」)が必要なのは明らかである。変位がなければ、様々なオブジェクトについて一つのコードを適用することができなくなる。つまり、パラメータとした型について、専用のメソッドを作成しなければならない。

ところがその一方で、安易に変位を行うと、型システムを破ってしまい、やすやすとランタイム例外が発生することになる。

Scalaの解決策

本来は、メソッドの処理内容によって、危険であるか安全であるかが決定するはずである。先のbadMethodは危険であるが、arraycopyは安全である。

ところが、Scalaではコンパイラが一律に安全か危険かを判断し、危険な場合はエラーとしてしまう。例えば、以下ようなクラスを書くとエラーになる。

class Animal
class Fish extends Animal
class Bird extends Animal
class Zoo[+T <: Animal] { // Tは共変的パラメータ
  def add(a:T) {} // ここでエラー!
}

たしかに、コンパイルが通ってしまうと以下のような操作ができる可能性がある。

val fishZoo = new Zoo[Fish]
val animalZoo:Zoo[Animal] = fishZoo
animalZoo add new Bird

実際には、addメソッドの中で危険な操作をしなければ大丈夫なはずだが、Scalaコンパイラは単に「共変的パラメータが反変的位置(メソッド引数)」に現れただけでエラーとする。

※コンパイラがどのような判断をするかは、日本語訳「Scalaスケーラブルプログラミング」の360ページあたりに解説があるのだが、この説明はちんぷんかんぷんである。現時点では「共変的パラメータはメソッド引数には使えない」とだけ覚えておく。