the industrial

都内で働くITエンジニアの日記

Scalaのimplicitについて

なんかいまさらこんなタイトルでブログ書くと、すっごくマニアックなものを見つけたのか!?とか思われそうだけど、いやいや、すっごく初歩的な内容ですすみません(汗)

Scalaには結構慣れて来た方で、ソースコードは随分読めるようになってきたのね(上達はしていない)。

だけど、いまいち理解が進んでいないものも多い。その一つがこのimplicit。

なんとなくわかるんだけど、ちゃんと分かっていない。それは大変危険でダメのこと。

なので、しっかり理解するためにも、ブログにアウトプットすることでしっかり覚えようと思う。

後で読み返してもいいし。

Version

試したScalaのVersionは2.11.4デス。

>scala
Welcome to Scala version 2.11.4 (Java HotSpot(TM) Client VM, Java 1.7.0_67).
Type in expressions to have them evaluated.
Type :help for more information.

implicitとは

発音記号「ɪmplísɪt」 【形容詞】 暗に示された,言わず語らずの,暗黙の (⇔explicit)

つまり、「暗黙の~」という意味ですかね。

3つのimplicit

Scalaでimplicitとつくものには、大きく分けて下記の3つがある(と思っている)。

  • implicit parameter
  • implicit conversion
  • implicit classes(Scala ver.2.10~)

implicit parameter(暗黙引数)

例えば値を受け取ってそのまま返却するメソッドがあるとする。

scala> def func(i: Int) = i
func: (implicit i: Int)Int

当たり前だけど、このメソッドfuncを利用する際は引数を指定する必要がある。

scala> func
<console>:9: error: missing arguments for method func;
follow this method with `_' if you want to treat it as a partially applied function
              func
              ^

scala> func(1)
res1: Int = 1

このメソッドの引数にimplicitをつけて、同じくimplicitがついた値を定義しておく。

scala>   def func(implicit i: Int) = i
func: (implicit i: Int)Int

scala>   implicit var implicitParam: Int = 50
implicitParam: Int = 50

すると、メソッド呼び出しの際implicitをつけた引数を省略してメソッドを呼び出す事が出来るようになり、下記の通りの結果になる。

※引数を指定しない場合はスコープの中から同じ型のimplicitな変数を探しに行き、implicitな引数にセットする。

scala> func(10)
res4: Int = 10

scala> func
res5: Int = 50
// ここでは上で定義した”implicitParam”の値(50)が引数にセットされた。

ルールとしては

  • implicitは最後の引数リストにしかつけられない
  • 同じスコープ内に同じ型のimplicitな変数は定義した場合コンパイルエラー
  • 関数リテラル”() => {}”の引数には定義出来ない(っぽい)

などなどかな。

こんな感じ?

// 通常
def func1(i: Int, implicit j: Int) = i + j          // ×
def func2(implicit i: Int, implicit j: Int) = i + j // ×
def func3(implicit i: Int, j: Int) = i + j          // ○
// カリー化後
def func4(implicit i: Int)(implicit j: Int) = i + j // ×
def func5(implicit i: Int)(j: Int) = i + j          // ×
def func6(i: Int)(implicit j: Int) = i + j          // ○

// 同じスコープだとして
implicit var implicitParam1: Int = 100 // ×
implicit var implicitParam2: Int = 200 // ×

// 関数リテラルはダメだった
val func = (implicit i: Int) => {i}

ちょっと分からない動き その1「すべての引数にimplicitが付与される」

func3定義時に、下記の様にそれぞれの引数にimplicitが付いた。

呼び出し方もこのくらいだった。この動きは気をつけないとハマりそう。

scala> def func3(implicit i: Int, j: Int) = i + j
func3: (implicit i: Int, implicit j: Int)Int

scala> func3
res9: Int = 100

scala> func3(1,1)
res10: Int = 2

ちょっと分からない動き その2「func5が定義出来ないのは何故だ」

何故func5が定義出来ないのかが分からない。

とりあえずfunc6の動きを確認する場合、こんな感じでしょう。 これは理解できる。

scala> func6(1)
res24: Int = 51

scala> func6(1)(1)
res25: Int = 2

scala> val func7 = func6(1)
res1: Int = 51

scala> def func6(i: Int)(implicit j: Int, k:Int) = i + j + k
func6: (i: Int)(implicit j: Int, implicit k: Int)Int
// func3と同じく、implicitのついた方の引数はすべてimplicitが付与された

そしてfunc5については、下記のような感じでイメージして利用できると思ったんだけどなあと。

scala> func5
// 引数”implicit i: Int”に50が設定され、"res1: (j: Int)Int"が返却されるイメージだった(返却のイメージあってるのかな

scala> func5(1)(1)
// 引数”implicit i: Int”のimplicitを無視して、"res1: Int = 2"が返却されるイメージだった

(2015/03/09 追記)→意味がわかった

ちょっと分からない動き その1「すべての引数にimplicitが付与される」

まず、implicitが1番目の変数にしかつけられないのではなく、変数すべてに作用すると言う意味で、引数リストの頭にimplicitと定義するから、これはクリア。

ちょっと分からない動き その2「func5が定義出来ないのは何故だ」

こちらは、”最後の引数リスト”にしかimplicitをつけられないというルールがあるから。

こちらを参考にさせていただきました。

Scalaのimplicit(暗黙)入門 - seratch's weblog in Japanese

こういう勘違いは怖い怖い。気付いて良かった。

implicit conversion(暗黙変換)

自動で型変換を行ってくれるメソッドを定義することが出来る。

定義されたimplicitメソッドは、スコープの中で生きる。

例えば、Int型をString型に変換するようなimplixit defを用意した場合、以降Int型の数値に対してStrringのメソッドを呼び出せる。

これは面白い。

implicit def intConvertToString(x: Int): String = x.toString
println(0.length())
// 1
println(1000.length())
// 4
println(1200.contains(20))
// true
println(1200.contains("20"))
// true

下記の場合、”intConvertToString3”は”intConvertToString1”と被るので定義出来ないとのこと。

implicit def intConvertToString1(x: Int): String = x.toString // ○
implicit def intConvertToString2(x: Int): Long = x.toLong // ○
implicit def intConvertToString3(x: Int): String = x.toString // ×

implicit classes(暗黙のクラス)

既存のクラスを拡張出来るような機能で、Scala2.10.0で追加される前まではPim My Libraryと言うものを利用していた。

たとえばこういう暗黙のクラスを用意しておけば

  implicit class AddString(target: String) {
    def addPrefix          = s"PREFIX_${target}"
    def addSuffix          = s"${target}_SUFFIX"
    def addPrefixAndSuffix = s"PREFIX_${target}_SUFFIX"
  }

Stringで下記の様に呼び出せるのだ。

implicitクラスの引数に指定した方(ここではtarget:StringのString)を拡張して、独自のメソッドが定義出来る。

println("TEST".addPrefix)
// PREFIX_TEST
println("TEST".addSuffix)
// TEST_SUFFIX
println("TEST".addPrefixAndSuffix)
// PREFIX_TEST_SUFFIX

ちなみに、先ほどのimplicit conversionとコラボ(型にimplicit classのAddStringを指定)してみたら、なんかすごいことになった。 まあ、Intを受け取って、toStringした結果を、独自のimplicit classの型(AddString)で返却してるだけなんだけど。

implicit def intConvertToString(x: Int): AddString = x.toString
println(0.addPrefix)
// PREFIX_0
println(0.addSuffix)
// 0_SUFFIX
println(0.addPrefixAndSuffix)
// PREFIX_0_SUFFIX

ここまでやるとわけわからなくなりそうなので、これはあまり使わない方向でいこう。

ただ、こんな書き方が出来るのはとても面白い。

やっぱScala面白い。

その他

間違った箇所などあったらご指摘いただけると嬉しいデス!

参考

主に下記を参考にさせていただきました。

Scala逆引きレシピ

暗黙のパラメーター(implicit parameter) - Scalaメモ(Hishidama's Scala Memo) - Ne

暗黙変換(implicit conversion) - Scalaメモ(Hishidama's Scala Memo) - Ne

Scala 2.10.0 M3の新機能を試してみる(2) - SIP-13 - Implicit classes - kmizuの日記

暗黙のクラス(implicit classes) - Scalaメモ(Hishidama's Scala Memo) - Ne