implicit value の定義方法いろいろ

このエントリは Scala Advent Calendar jp 2011 の四日目です。

Scala には implicit parameter という機能があります。

どういう機能かと言うと、引数に implicit という修飾子をつけると、関数呼び出しの際にその引数を省略することが可能になるというものです。

省略された引数はどう解決されるかというと、現在のスコープから見える範囲にあるこれまた implicit という修飾子がついた値からコンパイラが適切なものを選択して解決してくれます。

コンパイラが適切な値を選択できなかった場合、コンパイルエラーになります。

簡単な例を見てみましょう。

scala> trait ClassName[-A] { def apply(value: A): String }
defined trait ClassName

scala> def getClassName[A](value: A)(implicit className: ClassName[A]): String = className(value)
getClassName: [A](value: A)(implicit className: ClassName[A])String

メソッド getClassName は型パラメータAを取り、A型の value と ClassName[A]型の className を引数として受け付けます。そして戻り値に引数 className の apply を実行した結果を返します。

この二つ目の引数 className に implicit がついています。したがってスコープ内に適切な ClassName[A]型 のオブジェクトが implicit な値として存在していれば、getClassName は引数 value を与えるだけで実行することができるようになります。

ためしに実行してみましょう。

scala> getClassName("test")
<console>:10: error: could not find implicit value for parameter className: ClassName[java.lang.String]
       getClassName("test")
                   ^

エラーになりました。エラーメッセージを見てわかる通り、現在のスコープ内に ClassName[java.lang.String] 型の implicit な値が存在しないため、コンパイラが見つけられなかった訳ですね。

では ClassName[java.lang.String] 型の implicit な値を定義してみましょう。

scala> implicit val StringClassName = new ClassName[String] { 
     |   def apply(value: String) = "String(%s)".format(value)
     | }
StringClassName: java.lang.Object with ClassName[String] = $anon$1@4d8040

scala> getClassName("test")
res1: String = String(test)

めでたく実行することができました。

さてここからが本題です。このような implicit な値を定義することで implicit parameter の機能を使うことができるのですが、implicit な値の定義には implicit val だけではなく他の方法も使うことができます。その方法を紹介していきたいと思います。

まずは単純な object を使う方法。

scala> implicit object IntClassName extends ClassName[Int] {
     |   def apply(value: Int) = "Int(%d)".format(value) 
     | }
defined module IntClassName

scala> getClassName(10)
res2: String = Int(10)

シングルトンオブジェクトは言ってみれば唯のインスタンスの参照なので値として扱えるのも不思議ではありません。

そして実はメソッドも implicit な値として扱うことができます。

scala> implicit def LongClassName = new ClassName[Long] {
     |   def apply(value: Long) = "Long(%d)".format(value)
     | }
LongClassName: java.lang.Object with ClassName[Long]

scala> getClassName(3L)
res3: String = Long(3)

implicit def は implicit conversion の為だけに使われるとは限らないのですね。

implicit val と implicit def で何が違うかというと、まず implicit def は、implicit parameter として補完される度、毎回評価されます。

scala> implicit def BooleanClaeeName = {
     |   println("evaluateBooleanClassName")
     |   new ClassName[Boolean] { def apply(value: Boolean) = "Boolean(%s)".format(value) }
     | }
BooleanClaeeName: java.lang.Object with ClassName[Boolean]

scala> getClassName(true)
evaluateBooleanClassName
res4: String = Boolean(true)

scala> getClassName(false)
evaluateBooleanClassName
res5: String = Boolean(false)

そして特に大きな違いなのは、implicit def は型パラメータをとることができるという点です。

scala> implicit def OptionClassName[A] = new ClassName[Option[A]] {
     |   def apply(value: Option[A]) = "Option(%s)".format(value)
     | }
OptionClassName: [A]=> java.lang.Object with ClassName[Option[A]]

scala> getClassName(Some(10))
res6: String = Option(Some(10))

scala> getClassName(Some("test"))
res7: String = Option(Some(test))

さらに踏み込むと implicit def で定義した場合、そのメソッドにも implicit parameter を使うことができるのです。

scala> implicit def ListClassName[A](implicit c: ClassName[A]) = new ClassName[List[A]] {
     |   def apply(value: List[A]) = value.map(c(_)).mkString("List(", ", ", ")")
     | }
ListClassName: [A](implicit c: ClassName[A])java.lang.Object with ClassName[List[A]]

scala> getClassName(List(1, 2, 3))
res8: String = List(Int(1), Int(2), Int(3))

scala> getClassName(List(List(1, 2), List(), List(3, 4, 5)))
res9: String = List(List(Int(1), Int(2)), List(), List(Int(3), Int(4), Int(5)))

これを利用することで再帰的に implicit parameter を解決することが可能になります。

もちろん通常の再帰関数の様に、再帰の終了が存在しないようなケースではコンパイルエラーになるので注意が必要です。

scala> implicit def Product2ClassName[A, B](implicit ca: ClassName[A], cb: ClassName[B]) =
     |   new ClassName[Product2[A, B]] { 
     |     def apply(v: Product2[A, B]) = "(%s, %s)".format(ca(v._1), cb(v._2))
     |   }
Product2ClassName: [A, B](implicit ca: ClassName[A], implicit cb: ClassName[B])java.lang.Object with ClassName[Product2[A,B]]

scala> case class Foo(_1: Int, _2: Foo) extends Product2[Int, Foo]
defined class Foo

scala> getClassName(Foo(10, Foo(20, null)))
<console>:15: error: diverging implicit expansion for type ClassName[Foo]
starting with method Product2ClassName in object $iw
       getClassName(Foo(10, Foo(20, null)))
                   ^

以上の事を抑えておけば、自分のコードで implicit parameter を使いこなしたり、型クラスを多用するようなライブラリを読み解く助けになると思います。

ぜひ implicit parameter を活用してみてください。