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

独学大学情報学部

主にScala、たまにHaskell、稀に数学

暗黙の型変換(Implicit Conversion)のお話

この記事はPlay or Scala Advent Calendar 2012の18日目です。
何か作ってみるとか言いながら解説記事になりました(謝
総力尽くして色々解読してますが、補足・間違い等ありましたらご指摘ください。

暗黙のパラメータ(Implicit Parameter)についても一緒にまとめようと思ったけど、長くなりそうだったので省きます...。

そもそも何なのこの機能

語弊も承知で言うならばコンパイラが勝手にやってくれる型キャストみたいな機能」 です。こりゃ確実に刺されますね、はい。
もう少しちゃんと説明すると、Scalaコンパイラは型の不一致によるエラーを検出した際に悲鳴をあげる前にスコープ内のimplicitキーワードで定義されたメソッドを用いて解決を図ります。もし、解決出来る場合はそのメソッドを自動的に挿入して実行に移ります。 既存のライブラリ直接弄ることなく拡張できちゃうすんばらしぃ機能です。

種類とか例とか

Implicit Conversionは主に「要求された型への変換」と「レシーバーの変換」の二つに分けられます。

○ 要求された型への変換
求められている型と与える型が不一致だった場合に

// もちろんエラー
scala> val num: Int = "1"
<console>:7: error: type mismatch;
 found   : java.lang.String("1")
 required: Int
       val num: Int = "1"
                      ^

// 暗黙の型変換を定義すると
scala> implicit def string2int(s: String) = s.toInt
string2int: (s: String)Int

scala> val num: Int = "1"
num: Int = 1

ってな感じで暗黙の内に変換してくれます。便利ね!!
まぁ、実際こんな変換定義したら刺される程度じゃ済まn(ry

○ レシーバーの変換
与えられた文字列にわざわざ「Lonely」を付け加えて表示するウザいメソッドlonelyString型に追加したいとすれば

// レシーバーの変換 String -> LonelyString
scala> class LonelyString(str: String) {
     |   def lonely() = println("Lonely " + str + "!!")
     | }
defined class LonelyString

scala> implicit def string2lonelyString(s: String) = new LonelyString(s)
string2lonelyString: (s: String)LonelyString

scala> "Xmas".lonely
Lonely Xmas!!

ということで、あたかもStringlonelyメソッドが存在するかの如く振る舞ってくれちゃいます。凄いね!!

制限・条件

Inplicit Conversionを発動!させるにはもちろん制限・条件があります。

  • implicitキーワードが付けられた定義のみ、暗黙の型変換に利用される
  • 単一の識別子として上記の定義がスコープ内に存在している
  • 暗黙の型変換は一回のみ適用される(型変換の型変換で整合性を取ろうとしない)
  • 明示的変換等によって元々動作する場合、暗黙の型変換は適用されない

ちなみに

上記例のレシーバーの変換について、Scala 2.10では以下の様に書くことが出来ます。

scala> implicit class LonelyString(str: String) {
     |   def lonely() = println("Lonely " + str + "!!")
     | }
defined class LonelyString

scala> "Xmas".lonely
Lonely Xmas!!

これだけでいいの!?キャー簡潔っ///
あぁ...流行に乗り遅れてる感ががが...ちゃんと仕様追いかけます(´・ω・`)

ちょっと探検

だいたいおおよそおおざっぱにお分かり頂けたと思うので、超お馴染みなScalaString型がどのようにして「すげぇRich///」になってるのか見てみましょう。*1
コップ本にもこの話はありますので読むといいかもです。

まず、Scalaコンパイラは拡張子.scalaが付いたファイル全てに以下のインポート文を暗黙のうちに追加しています。

import java.lang._
import scala._
import Predef._

ScalaStringPredefの中に

// scala/src/library/scala/Predef.scala
package scala
// ...
object Predef extends LowPriorityImplicits {
  // ...
  type String = java.lang.String
  // ...
  @inline implicit def augmentString(x: String): StringOps = 
    new StringOps(x)
  // ...
}

の様に定義されています。java.lang.Stringに対するメソッド呼び出しは暗黙のうちにaugmentStringメソッドによって StringOpsに変換されます。StringOpsは数多のメソッドを含んだクラスで、 一通り眺めるだけでお腹一杯になれます。*2

Scala 2.7の頃のString型は現在のWrappedStringへ変換されていました。WrappedStringStringOpsreversefilterメソッドの返り血が異なるくらいで、あとは似たようなものかと。*3
違いを具体的に見るならば

// Scala 2.8以降は直感的に
scala> "abc".reverse.reverse == "abc"
// => true

// WrappedStringを使うと
scala> val str = new scala.collection.immutable.WrappedString("abc")
res0: scala.collection.immutable.WrappedString = abc

scala> str.reverse.reverse == "abc"
// => false

ってな感じでおわかりいただけただろうか(震え声

ただ、Scala 2.8以降はWrappedStringへの暗黙の型変換はPredefの親クラスLowPriorityImplicits

// scala/src/library/scala/LowPriorityImplicits.scala
package scala
// ...
class LowPriorityImplicits {
  // ...
  implicit def wrapString(s: String): WrappedString =
    if (s ne null) new WrappedString(s) else null
  implicit def unwrapString(ws: WrappedString): String =
    if (ws ne null) ws.self else null
  // ...
}

と定義されていますが、コンパイラPredefLowPriorityImplicitsの継承関係より Predefに定義されている型変換を行います。*4
ってことで、普段使う分にはあまり考慮する必要がないってことですね!

まとめ

\\ Scalaすげぇ // \\ Scalaすげぇ // \\ Scalaすげぇ //


*1:Scala Standard Library 2.9.2です。

*2:実際にはStringLikeトレイトまでで大半を実装してるのでStringOpsクラスのソース自体は非常に小さいです。

*3:WrappedStringStringLike[WrappedString]StringOpsStringLike[String]をmixinしています。

*4:複数の型変換が存在する時、コンパイラはサブクラス側の型変換を選択します。