Locked History Actions

scala/implicit

implicit

暗黙のパラメータ

implicitとマークされた引数は、その型と一致し、implicitとマークされたオブジェクトが存在すれば自動的に供給される。

object ImplicitTest {

  class Sample(value: Int) {
    override def toString = { "Sample:" + value }
  }

  // 暗黙的に供給されるオブジェクト。implicitを付けておかないとだめ
  implicit val toBeImplicitArg = new Sample(1);

  // implicitとマークされた引数を持つメソッド。
  def methodWithImplicitArg(implicit s: Sample) {
    println("methodWithImplicitArg called:" + s)
  }

  def main(args: Array[String]) {
    methodWithImplicitArg // 暗黙的に供給
    methodWithImplicitArg(new Sample(2)) // 明示的に供給
    // methodWithImplicitArg() ... この場合、()をつけてはだめ。引数が不足と言われてしまう。
  }
}

この結果は

methodWithImplicitArg called:Sample:1
methodWithImplicitArg called:Sample:2

供給側を関数に変更

上記の

  implicit val toBeImplicitArg = new Sample(1);

は、関数に変更しても機能する。

  implicit def toBeImplicitArg: Sample = new Sample(1)

供給側をオブジェクトに変更

供給側をオブジェクトに変更してもよい。

  implicit object toBeImplicitArg extends Sample(1) {}

つまり、implicitとマークされた引数に供給できるものは、implicitのvalに限ることなく、implicitのdefやimplicitのobjectでもよい。

その場に応じた引数値を生成する方法

上記の関数を使った場合の例では、固定的な値だけではなく、その場で値を作成することもできそうだが、 このままでは同じ値「new Sample(1)」しか作成されない。 呼び出される「文脈」をtoBeImplicitArg関数に与え、その場に応じた値を作成できないとまったく面白くはない。 つまり、以下のように記述して、valueの値を指定できないものだろうか。

 implicit def toBeImplicitArg(value: Int): Sample = new Sample(value)

ところが、これは一般に不可能のようである(たぶん)。ただし、A Tour of Scala: Implicit Parametersを見てみると、型パラメータの値によって引数値を選択することはできるようだ。この例をもう少し単純化してみる。

abstract class Add[A]  {
  def add(x: A, y: A): A
}
object ImplicitTest  {
  implicit object AddString extends Add[String] { def add(x: String, y: String) = x + y }
  implicit object AddInt extends Add[Int]      { def add(x: Int, y: Int) = x + y }
  def sum[A](a: A, b: A)(implicit m: Add[A]) = m.add(a, b)
  
  def main(args: Array[String]) {
    println(sum(1, 2))
    println(sum("a", "b"))
  }
}

この結果は、

3
ab

つまり、型パラメータによってString型を処理するAddStringか、Int型のAddIntが自動選択される。

特殊な暗黙のパラメータ

Manifest及びClassManifestについては、その取得方法が提供されていなくとも、自動的に引数値が生成される。

object ImplicitTest  {
  def manifestTest[T](implicit manifest: Manifest[T]) {
    println(manifest)
  }
  def main(args: Array[String]) {
    manifestTest[List[String]];
  }
}

この実行結果は

scala.collection.immutable.List[java.lang.String]

型パラメータが二つある場合は

object ImplicitTest  {
  def manifestTest[T, S](implicit tman: Manifest[T], sman: Manifest[S]) {
    println(tman + "\n" + sman);
  }
  def main(args: Array[String]) {
    manifestTest[List[String], Int];
  }
}

結果は

scala.collection.immutable.List[java.lang.String]
Int

Manifest/ClassManifestとは何か

要するに、Javaでジェネリックな型を使用した場合のクラス引数の代わりになるものである。 どういうことかというと、たまに以下のようなコードを書きたくなることがある。

public class JavaTest {
  static class Sample {}
  static class InstanceGenerator<T> {
    private Class<T>clazz;
    public InstanceGenerator(Class<T>clazz) {
      this.clazz = clazz;
    }
    public T generate() {
      try {
        return (T)clazz.newInstance();
      } catch (Exception ex) {
        throw new RuntimeException(ex);
      }
    }
  }
  public static void main(String[]args) {
    InstanceGenerator<Sample> gen = new InstanceGenerator<Sample>(Sample.class);
    System.out.println(gen.generate());
  }
}

結果は

JavaTest$Sample@a62fc3

ここで面倒なのが、型パラメータでSampleという型を指定しているにも関わらず、コンストラクタ引数としてSample.classを与えていることである。Javaでは、型パラメータとして与えた型情報をInstanceGenerator内部で利用できないのである。これは「イレージャ」というもののせいだという。(ただし、サブクラス化すれば問題ない。これについてはジェネリックスの情報取得を参照)。

Scalaでは以下のように記述できる。

object ScalaTest {
  class Sample {}
  class InstanceGenerator[T](implicit manifest: Manifest[T]) {
    def generate = { manifest.erasure.newInstance().asInstanceOf[T] }
  }
  def main(args: Array[String]) {
    val gen = new InstanceGenerator[Sample]
    println(gen.generate);
  }
}

結果は

ScalaTest$Sample@93dcd

Scalaでも引数として型パラメータとして指定されたクラスのクラス実体(?)を表すオブジェクトを引き渡さなければならないのだが、これをimplicitとすることにより、いちいち手で指定してやる必要がなくなる、ということ。

これに対し、C#等ではこれを「こっそり」行っており、おそらく普通にnewするだけで型パラメータのクラス情報をオブジェクトに埋め込んでしまうらしいが(使ったことが無いので間違ってるかも)、これはおかしな話に思える。むしろScalaの方がまっとうな方法だと思うのだが。。。

ちなみにだが、一つの関数で生成するにはこうすればよい。

object ScalaTest {
  class Sample {}
  def generate[T](implicit manifest: Manifest[T]) = {
    manifest.erasure.newInstance().asInstanceOf[T]
  }
  def main(args: Array[String]) {
    println(generate[Sample]);
  }
}