暗黙の型変換(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」を付け加えて表示するウザいメソッドlonelyをString型に追加したいとすれば
// レシーバーの変換 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!!
ということで、あたかもStringにlonelyメソッドが存在するかの如く振る舞ってくれちゃいます。凄いね!!
制限・条件
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!!
これだけでいいの!?キャー簡潔っ///
あぁ...流行に乗り遅れてる感ががが...ちゃんと仕様追いかけます(´・ω・`)
ちょっと探検
だいたいおおよそおおざっぱにお分かり頂けたと思うので、超お馴染みなScalaのString型がどのようにして「すげぇRich///」になってるのか見てみましょう。*1
コップ本にもこの話はありますので読むといいかもです。
まず、Scalaコンパイラは拡張子.scalaが付いたファイル全てに以下のインポート文を暗黙のうちに追加しています。
import java.lang._ import scala._ import Predef._
// 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へ変換されていました。WrappedStringとStringOpsはreverseやfilterメソッドの返り血が異なるくらいで、あとは似たようなものかと。*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 // ... }
と定義されていますが、コンパイラはPredefとLowPriorityImplicitsの継承関係より Predefに定義されている型変換を行います。*4
ってことで、普段使う分にはあまり考慮する必要がないってことですね!
まとめ
\\ Scalaすげぇ // \\ Scalaすげぇ // \\ Scalaすげぇ //