Scala には暗黙の型変換機能がある。
例を挙げてみよう。
abstract class Monoid[A] { def add(x: A, y: A): A def unit: A }
Monoid クラスは、二つの機能を持つ。
[A] の部分が Generics になっていて、任意の型に適用することができる。
以下が String 版 Monoid。
object stringMonoid extends Monoid[String] { def add(x: String, y: String): String = x.concat(y) def unit: String = "" }
以下が Int 版 Monoid。
object intMonoid extends Monoid[Int] { def add(x: Int, y: Int): Int = x + y def unit: Int = 0 }
これを使った sum 関数を定義してみよう。
def sum[A](xs: List[A])(m: Monoid[A]): A = if (xs.isEmpty) m.unit else m.add(xs.head, sum(xs.tail)(m))
リストおよび Monoid を引数に取り、リストの合計を返す。
使うときは以下のようにする。
sum(List("a", "bc", "def"))(stringMonoid) sum(List(1, 2, 3))(intMonoid)
しかしなんかやぼったい。
前者は String のリストを引数に渡しているのだから stringMonoid だと推測してくれても良さそうなものである。
それを実現するのが implicit キーワード。
def sum[A](xs: List[A])(implicit m: Monoid[A]): A = if (xs.isEmpty) m.unit else m.add(xs.head, sum(xs.tail))
このように、推測してほしい変数に implicit キーワードを付ける。
ただしこれだけではダメ。推測するにはそれなりの準備が必要だ。
implicit object stringMonoid extends Monoid[String] { ... } implicit object intMonoid extends Monoid[Int] { ... }
このように、Monoid の定義にも implicit を付ける。
これで、これらのオブジェクトは型推測のターゲットとなる。
sum(List("a", "bc", "def"))
今度は上のコードで、処理は成功する。
第一引数が List[String] だから、sum の A は String に決定する。
すると Monoid[String] を満たすオブジェクトはこのスコープ内には stringMonoid しか無いから
第二引数には stringMonoid が暗黙のうちに渡される。
また、implicit は関数に付けることもできる。
implicit def int2ordered(x: Int): Ordered[Int] = new Ordered[Int] { def compare(y: Int): Int = if (x < y) 1 else if (x > y) 1 else 0 }
このように、引数を一つ取り値を返す単純な型変換関数に implicit キーワードを適用する。
こうしておくことで、以下のような関数を定義できる。
def sort[A <% Ordered[A]](xs: List[A]): List[A] = { for (x <- xs) { ... if (x < ...) ... } }
[A <% Ordered[A]] とは、Ordered[A] に変換可能な A という意味。
これを View Bounds と言う。
sort(List(1, 2, 3))
呼び出し側はこのように記述できる。1 などの数値は Int だから通常は Ordered の trait など持っていない。
しかし View Bounds を使うことによって、sort 関数の中で A は Ordered[A] に変換される(int2orderedを通して)。
よって、コードにあるように
(x < ...)
といった記述が可能となる。(< は Ordered が持つ演算子)
以下のようなコードがある。
(1 to height)
しかし、scala.Int には to なんて関数は定義されていない。
一体どうなっているのか?
scala.runtime.RichInt というクラスがあり、そこに以下の関数が定義されている。
def to (end: Int): Inclusive
Inclusive は Range のサブクラスだ。
ここでは、1 という数値が暗黙の型変換によって RichInt に変換され
to 関数を呼び出すことに成功している。
では、型変換関数はどこで定義されているのか?
答えは scala.Predef である。
implicit def intWrapper (x: Int): RichInt
PreDef は標準で import されるから、
これらの型変換はどのスコープでも利用可能となっている。
implicit は非常に便利だ。
しかし、多用は禁物だ。知らず知らずのうちに意図していない型変換が行われているかもしれない。