S-JIS[2011-01-04/2013-06-08] 変更履歴

Scalaジェネリクス(パラメータ化された型)

Javaで言うジェネリクス(総称型)は、Scalaでは「パラメータ化された型(parameterized types)」と呼ぶらしい。
(でもなんか長ったらしいので、自分はひとまずジェネリクスと呼ぶことにする^^;)

Scalaのジェネリクスは、型パラメーター(型引数)や型名を角括弧「[]」で囲む。(Javaは「<>」で囲む)
Scalaでは、型名を囲む必要がある場合は全て角括弧に統一されているようだ。(classOf[]isInstanceOf[]等)


クラスのジェネリクス

クラストレイトのジェネリクスは、クラス名(トレイト名)の後ろに角括弧で型パラメーターを記述する。

class クラス名[型パラメーター, …]trait トレイト名[型パラメーター, …]
Scalaの例 Java相当 備考
class Example[T] {
  protected var t: T = _

  def set(t: T): Unit = {
    this.t = t
  }

  def get(): T = {
    t
  }
}
public class Example<T> {
  protected T t;

  public void set(T t) {
    this.t = t;
  }

  public T get() {
    return t;
  }
}
型パラメーターが1つのクラスを定義した例。
val s = new Example[String]
        s.set("abc")
val v = s.get()
final Example<String> s = new Example<String>();
                 s.set("abc");
final String v = s.get();
使う際に型パラメーターを指定する例。
def f(s:Example[_]) = println(s)
public void f(Example<?> s) {
  System.out.println(s);
}
使う際に型パラメーターが何でもいい場合は、「_」を指定する。
class Example[T] extends Parent[T] {
}
public class Example<T> extends Parent<T> {
}
親クラスの型パラメーターに自分の型パラメーターを指定する例。
class Example[T, U] {
  def method1(t: T): Unit = {
    println(t)
  }
  def method2(u: U): Unit = {
    println(u)
  }
}
public class Example<T, U> {
  public void method1(T t) {
    System.out.println(t);
  }
  public void method2(U u) {
    System.out.println(u);
  }
}
型パラメーターが2つのクラスを定義した例。
val s = new Example[String, Int]

val s = new Example[String, Int]()
final Example<String, Integer> s =
  new Example<Stirng, Integer>();
型パラメーターが2つのクラスのインスタンスを生成する例。
val s = new (String Example Int)

val s = new (String Example Int)()
  型パラメーターが2つのクラスをnewでインスタンス生成する場合、クラス名を中央に置く書き方(中置記法)が出来る。
(まるで演算子のようになる。)
参考: okomokさんのOr.scala

new (A op B)は、AやBのインスタンスを生成するのではなく、opのインスタンスを生成している。
scala> class op[L, R]
defined class op

scala> class A; class B; class C
defined class A
defined class B
defined class C

scala> new (A op B)
res1: op[A,B] = op@138d2fc

scala> new (A op B op C)
res2: op[op[A,B],C] = op@d76d51

メソッドのジェネリクス

メソッドのみに適用するジェネリクスは、メソッド名の後ろに角括弧で型パラメーターを記述する。

def メソッド名[型パラメーター, …](引数, …) 〜
Scalaの例 Java相当 備考
class Example {
  def method[T](t: T): T = {
    t
  }
}
public class Example {
  public <T> T method(T t) {
    return t;
  }
}
型パラメーターが1つのクラスを定義した例。
val obj = new Example
val s = obj.method("abc")
final Example obj = new Example();
final String s = obj.method("abc");
メソッドのジェネリクスは、JavaでもScalaでも型推論してくれる。
val s = obj.method[String](null)
final String s = obj.<String>method(null);
型パラメーターを明示して呼び出す例。

型パラメーターの境界

型パラメーターの上限境界(upper bound、Javaのextends)・下限境界(lower bound、Javaのsuper?)について。
型パラメーターは、指定できるクラスの上限・下限(親クラス・子クラス)の範囲を指定することが出来る。

例として、A1←A2←A3という継承関係のクラスがあるとする。

class A1
class A2 extends A1
class A3 extends A2
Scalaの例 Java相当 備考
class Example[T] {
}
○ new Example[Any]
○ new Example[A1]
○ new Example[A2]
○ new Example[A3]
○ new Example[Nothing]
class Example<T> {
}
○ new Example<Object>();
○ new Example<A1>();
○ new Example<A2>();
○ new Example<A3>();
境界を何も指定しない例。
この場合、上限境界にAny、下限境界にNothingを指定したのと同じ。
class Example[T <: A2] {
}
× new Example[Any]
× new Example[A1]
○ new Example[A2]
○ new Example[A3]
○ new Example[Nothing]
class Example<T extends A2> {
}
× new Example<Object>();
× new Example<A1>();
○ new Example<A2>();
○ new Example<A3>();
上限境界を指定した例。
上限境界で指定したクラス自身とその派生クラスだけ受け付ける。
class Example[T >: A2] {
}
○ new Example[Any]
○ new Example[A1]
○ new Example[A2]
× new Example[A3]
× new Example[Nothing]
  下限境界を指定した例。
下限境界で指定したクラス自身とその親クラスだけ受け付ける。
(Javaではextendsと同じようなsuperの指定は出来ない)
class Example[T >: A3 <: A1] {
}
× new Example[Any]
○ new Example[A1]
○ new Example[A2]
○ new Example[A3]
× new Example[Nothing]
  下限境界と上限境界を同時に指定した例。
この場合、順序を変えて「T <: A1 >: A3」と書くとエラーになる。
class Example[T <% scala.runtime.RichInt] {
}
× new Example[Any]
○ new Example[scala.runtime.RichInt]
○ new Example[Int]
○ new Example[Nothing]
  可視境界(view bound)の例。
FからCへの暗黙変換(例:IntRichInt)が定義されている場合、
T <% C」と書けば、Fも型パラメーターに指定できるようになる。

implicitへの書き換え
//上限境界では、暗黙変換は無関係
class Example[T <: scala.runtime.RichInt] {
}
× new Example[Any]
○ new Example[scala.runtime.RichInt]
× new Example[Int]
○ new Example[Nothing]
class Example[T]
def fu(s: Example[_ <: A2]) = println(s)
× fu(new Example[Any])
× fu(new Example[A1])
○ fu(new Example[A2])
○ fu(new Example[A3])
○ fu(new Example[Nothing])
def fl(s: Example[_ >: A2]) = println(s)
○ fl(new Example[Any])
○ fl(new Example[A1])
○ fl(new Example[A2])
× fl(new Example[A3])
× fl(new Example[Nothing])
def fb(s: Example[_ >: A3 <: A1]) = println(s)
× fb(new Example[Any])
○ fb(new Example[A1])
○ fb(new Example[A2])
○ fb(new Example[A3])
× fb(new Example[Nothing])
class Example<T> {}
public void fu(Example<? extends A2> s) {
  System.out.println(s);
}
public void fl(Example<? super A2> s) {
  System.out.println(s);
}
メソッドの引数などで使用する際に境界を指定する事も出来る。

(Javaでは、extendsとsuperを同時に指定する事は出来ない)

この使い方の場合、「<%」を指定する事は出来ないようだ。
import java.io._

class Example[T <: Serializable with Closeable] {
}
class SC extends Serializable with Closeable {


  def close() = {}
}

new Example[SC]
import java.io.*;

class Example<T extends Serializable & Closeable> {
}
class SC implements Serializable, Closeable {

  @Override
  public void close() throws IOException {}
}

new Example<SC>();
Javaでは、複数のインターフェースを継承していることを示したい場合は「&」でつなぐ。
Scalaでは、素直に“withでつないだ新しいクラス(トレイト)”を上限境界に指定すれば良さそう。
class Example[T <: { def close():Unit }] {
}
  型パラメーターには、クラス名そのものだけではなく、クラス定義を直接指定することが出来る。
ただし、これとwithを同時に使うことは出来ないようだ。
class Example[T : Hoge] {
}
  Scala2.8で導入されたcontext bound

type(抽象タイプ)での上限境界(<:)・下限境界(>:


型パラメーターの制約

型パラメーターが特定のクラスのときだけ呼び出せるようなメソッドを作る(そういう制限をする)ことが出来る。[2011-07-24]
これをGeneralized Type Constraintsと言う。(日本語訳では何と呼ぶのか定まっていないようなので、造語として「型パラメーター制約」と呼ぶことにする)


どういうときに使うかと言うと、ListtoMapメソッドの例が分かり易い。
Listの定義は(単純化して言うと)「class List[A]」なので、Listの要素にはどんなクラスでも指定することが出来る。
ところがtoMapメソッドは、要素がTuple2のときだけしか使えない。
“使えない”と言うのは、実行時に例外が発生するという意味ではなく、Tuple2以外の型だったらコンパイルエラーになるということ。
これを型パラメーターの制約で実現している。

class List[A] {
  def toMap[K,V](implicit ev: A <:< (K,V)): Map[K,V] = 〜
}

<:<」が制約を表している。
A <:< (K,V)」は、「Aが(K,V)(タプル)の子クラスである(タプルを継承している)必要がある」という制約を意味する。

そういう条件を満たすような暗黙の値を探し、見つかればOK(コンパイルが通る)、見つからなければNG(コンパイルエラー)になるという仕組み。
ちなみに引数の変数名の「ev」はevidence(証拠)の事らしい。実行時には何らかのインスタンスが渡ってくるので、それが渡ってくるのは「コンパイルが通った証拠」というようなニュアンスなんだろうか。


型パラメーター制約の記号
記号 説明
A =:= B AとBが等しい。
A <:< B AはBの子クラス(サブクラス)である。
A <%< B Aは暗黙変換によってBになれる。

なお、これらの記号の実態は、Predefに定義されたクラス。(Predefに定義されているので、自動的に使える状態になっている)
(型パラメーターが2個のクラスでは中置記法が使える。つまり「C[A,B]」は「A C B」と書ける。Cが記号なら「=:=[A,B]」「A =:= B」ということになる)
これらに適合させる為の暗黙変換関数もPredef内に定義されている。


変位指定

Javaでは配列は共変なので、String配列(String[])をObject配列の変数(Object[])に代入することが出来る。
また、Javaのジェネリクスは共変でないので、例えばStringのList(List<String>)はObjectのListの変数(List<Object>)に代入することは出来ない。
(StringはObjectを継承しているが、StringのコレクションはObjectのコレクションに代入できるか?という問題。なお、Javaでは配列が共変なのは危険だとされている。
Scalaの配列はJavaと異なり、非変である)

Scalaでは、変位指定(変位指定アノテーション、variance annotations)によって非変(nonvariant)・共変(covariant)・反変(contravariant)を指定することが出来る。

例として、A1←A2←A3という継承関係のクラスがあるとする。

class A1
class A2 extends A1
class A3 extends A2
Scalaの例 Java相当 備考
class MyList[T] {
}
var list:MyList[A2] = _
× list = new MyList[A1]
○ list = new MyList[A2]
× list = new MyList[A3]
class MyList<T> {
}
MyList<A2> list = null;
× list = new MyList<A1>();
○ list = new MyList<A2>();
× list = new MyList<A3>();
型パラメーターに特に何も指定しないと、非変(nonvariant)になる。
この場合、変数に指定されている型にしか代入できない。

(ちなみに、ScalaのListは(不変オブジェクトなので)共変
 
List<String> list = new ArrayList<String>();
class MyArray[+T] {
}
var array:MyArray[A2] = _
× array = new MyArray[A1]
○ array = new MyArray[A2]
○ array = new MyArray[A3]

		
A2[] array = null;
× array = new A1[] {};
○ array = new A2[] {};
○ array = new A3[] {};
型パラメーターの前に「+」(共変アノテーション)を付けると共変(covariant)になる。
子クラスのコレクションを親クラスのコレクションに代入できる。

関連?→共変戻り値型
 
Object[] array = new String[] {};
class MyList[-T] {
}
var list:MyList[A2] = _
○ list = new MyList[A1]
○ list = new MyList[A2]
× list = new MyList[A3]
  型パラメーターの前に「-」(反変アノテーション)を付けると反変(contravariant)になる。
親クラスのコレクションを子クラスのコレクションに代入できる。

参考: Naotsuguさんの型パラメータの変位指定 …特に最後のまとめが使い分けの参考になる


Javaのジェネリクスで出来ない事

Javaのジェネリクスでは出来ない事がある。Scalaではどうか?[/2013-06-08]

  Scala相当 Javaの例 備考
クラス取得
//×出来ない(コンパイルエラー)
class Example[T] {
  def printClass(): Unit = {
    val c = classOf[T]
    println(c)
  }
}
//×出来ない(コンパイルエラー)
public class Example<T> {
  public void printClass() {
    final Class c = T.class;
    System.out.println(c);
  }
}
型パラメーターからjava.lang.Classを取得することは出来ない。
class Example[T](implicit m: ClassTag[T]) {
  def printClass(): Unit = {
    val c = m.runtimeClass
    println(c)
  }
}
val s = new Example[String]
s.printClass()
  ScalaではClassTagを使えば型パラメーターを受け取ることが出来るので、
そこからjava.lang.Class(型消去されたもの)を取得できる。
new
//×出来ない(コンパイルエラー)
class Example[T] {
  def create(): T = {
    new T()
  }
}
//×出来ない(コンパイルエラー)
public class Example<T> {
  public T create() {
    return new T();
  }
}
型パラメーターからnewでインスタンスを生成する事は出来ない。
class Example[T](implicit m: ClassTag[T]) {
  def create(): T = {
    m.runtimeClass.newInstance().asInstanceOf[T]
  }
}
val s = new Example[String]
val v = s.create()
  ClassTagを使えばjava.lang.Classが取得できるので、
後はリフレクション(newInstance())を用いればインスタンスを生成できる。
ただし、型パラメーターに指定するクラスに引数無しのpublicコンストラクターが必要。
(例えばIntは生成できない)

class Example[T](implicit m: ClassTag[T]) {
  def create(size:Int): Array[T] = {
    m.newArray(size)
  }
}
val s = new Example[Int]
val a = s.create(10)
//×出来ない(コンパイルエラー)
public class Example<T> {
  public T[] create(int size) {
    return new T[size];
  }
}
配列の生成なら、ClassTagにそれ専用のメソッドがある。
クラス判断
//×コンパイルは通るが、正しく動作しない
class Example[T] {
  def check(o:AnyRef): Boolean = {
    o.isInstanceOf[T]
  }
}
//×出来ない(コンパイルエラー)
public class Example<T> {
  public boolean check(Object o) {
    return (o instanceof T);
  }
}
型パラメーターを使って、インスタンスがその型かどうかを判断する事は出来ない。
  def check(o:AnyRef): Boolean = o match {
    case _:T => true
    case _   => false
  }
  instanceOfでの判断と同様、期待した動作にならない。[2011-06-25]
class Example[T](implicit m: ClassTag[T]) {
  def check(o:AnyRef): Boolean = {
    (o ne null) && m.runtimeClass.isInstance(o)
  }
}
val d = new Example[java.util.Date]
assert( d.check(new java.util.Date()) )
assert( d.check(new java.sql.Date(0)) )
assert(!d.check("abc") )
assert(!d.check(null) )

val q = new Example[java.sql.Date]
assert( q.check(new java.sql.Date(0)) )
assert(!q.check(new java.util.Date()) )
  ClassTagを使えばjava.lang.Classが取得できるので、
リフレクション(isInstance())を用いればクラスを判断できる。
class Example[T](implicit m: ClassTag[T]) {
  def check[S](o:S)(implicit s: ClassTag[S]): Boolean = {
    m.runtimeClass.isAssignableFrom(s.runtimeClass)
  }
}
  双方のClassTagを取得してみた例。
static  
//×出来ない(コンパイルエラー)
public class Example<T> {
  private static T value;
}

 

Javaではstaticなフィールドやメソッドにはクラスの型パラメーターを適用できない。
が、Scalaにはstaticなメンバーという概念は無いので、無関係。

Scala目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま