読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:ドワンゴ 新人向けScala研修テキスト

学習記録 関数型 プログラミング Scala

15. Implicit

Scalaにはimplicit Conversion(暗黙の型変換)とimplicit parameter(暗黙のパラメータ)という機能がある。
この2つを使いこなすことで,Scalaでのプログラミングの生産性は劇的に向上する。

Implicit Conversion

暗黙の型変換機能をユーザが定義できるようにする機能。
implicitというキーワードと引数が1つだけのことを除けば通常のメソッド定義と同じになる。

implicit def メソッド名(引数名: 引数の型): 返り値の型 = 本体

上記の定義により,引数の型の式が現れたとき,返り値の型を暗黙の型変換候補として登録することになる。
定義したimplicit conversionは大きく分けて2通りの使われ方をする。
1つは,新しく定義したユーザ定義の型などを既存の型に当てはめたい場合になる。

scala> implicit def intToBoolean(arg: Int): Boolean = arg != 0
<console>:12: warning: implicit conversion method intToBoolean should be enabled
...
intToBoolean: (arg: Int)Boolean

scala> if (1) {
     |   println("1 == true")
     | }
1 == true

こうすることで,本来Booleanしか渡せないはずのif式にIntを渡すことができる。しかし,Boolean型の式しか渡せないようにコンパイラがチェックしている部分を通りぬけてしまうことができることから,この使い方はあまり良いものではない。

pimp my library

もう1つの使い方はpimp my libraryパターンと呼ばれ,既存のクラスにメソッドを追加して拡張する(ようにみせる)使い方で,こちらが本来の使い方といってもよい。
例えば,(1 to 5)という式のtoメソッドはこのパターンの最たる例で,本来Int型が持たないtoメソッドを使えるようにしている。

試しに,Stringの末尾に"^^"という文字列を追加して返すimplicit conversionを定義してみる。

scala> class RichString(val src: String) {
     |   def smile: String = src + "^^"
     | }
defined class RichString

scala> implicit def enrichString(arg: String): RichString = new RichString(arg)
<console>:14: warning: implicit conversion method enrichString should be enabled
enrichString: (arg: String)RichString

scala> "Hi, ".smile
res2: String = Hi, ^^

ちゃんと文字列の末尾に"^^"を追加するsmileメソッドが定義できている。
コンパイラは,ある型に対するメソッド呼び出しを見つけたとき,そのメソッドを定義した型がimplicit conversionの返り値の型にないか探索し,型が合ったらimplicit Conversionの呼び出しを挿入する。

Implicit Class

上の定義は,Scala2.10以降ではclassにimplicitというキーワードをつけて書くことができる。

scala> implicit class RichString(val src: String) {
     |   def smile: String = src + "^^"
     | }
defined class RichString

scala> "uwaa".smile
res0: String = uwaa^^

implicit classはpimp my libraryパターン専用の機能で,implicit defで既存型への変換をした場合などによる混乱がないため,pimp my libraryパターンを使うときにはこちらの形式にした方がよい。

Implicit Parameter

implicit parameterの使い方も2つあり,1つ目はあちこちのメソッドに共通で引き渡されるオブジェクト(ソケットやコネクションなど)を明示的に引き渡すのを省略するために使うものになる。
例えば,データベースとのコネクションを表すConnection型があり,データベースと接続するメソッドは全てこのConnection型を引き渡す必要があるとする。

def useDatabase1(..., conn: Connection)
def useDatabase2(..., conn: Connection)
...

これらのメソッドは呼び出す度に明示的にConnectionオブジェクトを渡さなければならないのが面倒である。そこで,

def useDatabase1(...)(implicit conn: Connection)
def useDatabase2(...)(implicit conn: Connection)
...

とする。implicit修飾子は引数の先頭要素に付けなければならず,またカリー化されたメソッド定義が必要になる。
最後の引数リストが(implicit conn: Connection)とあるのがポイントで,Scalaコンパイラはこう定義されたメソッドが呼び出されると,現在のスコープから辿って直近のimplicitとマークされた値を暗黙にメソッドを引き渡す。
「値をimplicitとしてマークする」とは次のようにして行う。

implicit val connection: Connection = connectDatabase(...)

こうすることで,最後の引数リストに暗黙にConnectionオブジェクトを渡してくれる。このような使い方は,各種O/Rマッパーで頻出する。

scala> case class Connection(connStr: String)
defined class Connection

scala> def useDatabase(implicit conn: Connection)
     | = { 
     |   println("use: " + conn.connStr)
     | }
useDatabase: (implicit conn: Connection)Unit

scala> implicit val conn: Connection = new Connection("hogeDb")
conn: Connection = Connection(hogeDb)

scala> useDatabase
use: hogeDb

もう一つの使い方は少々変わっている。例えばListの全ての要素を加算するsumメソッドを定義したいとする。このメソッドのポイントは,何のリストか全くわかっていないため,整数の+メソッドをそのまま使ったりすることはできないということだ。このような場合,二つの手順を踏む。

まず,2つの同じ型を足す方法を知っている型,Additiveを定義する。

scala> trait Additive[A] {
     |   def plus(a: A, b: A): A
     |   def zero: A
     | }
defined trait Additive

ここで,型パラメータAは加算されるListの要素の型を表す。また,zero: Aの0に相当する値を返す,plus: Aを持つ2つの値を加算して返す,というメソッドを持つ。

次に,このAdditiveを使ってListの全ての要素を合計するメソッドを定義する。

scala> def sum[A](lst: List[A])(m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))
sum: [A](lst: List[A])(m: Additive[A])A

最後に,それぞれの型に応じた加算と0の定義を持ったobjectを定義する。

scala> object StringAdditive extends Additive[String] {
     |   def plus(a: String, b: String): String = a + b
     |   def zero: String = ""
     | }
defined object StringAdditive

scala> object IntAdditive extends Additive[Int] {
     |   def plus(a: Int, b: Int): Int = a + b
     |   def zero: Int = 0
     | }
defined object IntAdditive

さて,このsumメソッドを呼び出したいときには,以下のようにすればよい。

scala> sum(List(1,3,5))(IntAdditive)
res8: Int = 9

scala> sum(List("a","bb","ccc"))(StringAdditive)
res10: String = abbccc

しかし,Listの要素の型は型チェックする時点でわかっているのだから,わざわざIntAdditiveなどと明示的に渡さなくても推論してほしいものだ。そこで,implicit parameterを使う。
方法は簡単で,String/IntAdditiveの定義の前にimplicitと付け,sumの最後の引数のリストのmにimplicitを付けるだけである。

implicit object IntAdditive extends Additive[Int] { ... }
implicit object StringAdditive extends Additive[String] { ... }
def sum[A](lst: List[A])(implicit m: Additive[A]) = lst.foldLeft(m.zero)((x, y) => m.plus(x, y))

scala> sum(List(1,5,9))
res11: Int = 15

scala> sum(List("Alice","Bob","Curl"))
res12: String = AliceBobCurl