既存クラスを拡張(したように見せる)ひとつの方法
Rubyの特異メソッド/特異クラスみたいなことが出来ないのか、と思って調べていたところ、初期段階として「既存のクラスを拡張できるか」という点を掘ってみることになりました。
Scalaはもともと、RichStringというクラスがあり、java.lang.Stringクラスに存在しない便利メソッドが定義されており、必要に応じてRichStringに変換されて呼び出せるようになっています。
scala> "100" res24: java.lang.String = 100 scala> "100".r res25: scala.util.matching.Regex = 100
java.lang.Stringには r というメソッドは定義されていません。ここでは暗黙のうちにRichStringクラスへの変換が行われています。どのようなimplicit defが起こり得るか、を実行時にうまく知る方法があればいいのですが、私はよくわかりません。いろいろ実験してみた結果。
scala> val a = "100".r _ <console>:5: error: _ must follow method; cannot follow scala.util.matching.Regex val a = "100".r _ ^ scala> val a = ("100":RichString).r _ a: () => scala.util.matching.Regex = <function>
いささか明示的に追いかけてみたところですが、前者がエラーになるのが悲しいところです。
後者ではどのクラスに変換されうるかを知っていて、それを明示的に示している例です。実際には "r"メソッドを期待しているということは、何か特定のclass/traitを相手にしていることは間違いないのでさほど問題はないのですが。
さて、上記の例を見ると Stringクラスに rというメソッドを追加/拡張しているように見えます。これと同じことを自分でもやりたい!
$ ruby class String def hoge; 100 end end puts "hoge".hoge ^D 100
とかやりたい!という時にどうするか、が今回のテーマです。
いろいろ方法はありそうですが、RichStringの例に倣ってみます。
scala> class HogeString(val self:String) { | def hoge = 100 | } defined class HogeString scala> implicit def stringAsHogeString(src:String):HogeString = new HogeString(src); stringAsHogeString: (String)HogeString scala> "fuga" res1: java.lang.String = fuga scala> "fuga".hoge res2: Int = 100
うまくいきましたね!キモは implicit def です。この宣言がされたスコープ内では引数型と関数の戻り値の型の間で暗黙のうちに変換がおこなわれる、という仕掛けのようです。正直、かなりコワい機能でもあると思いますが。
さらに突っ込んで簡単にする方法もあります
scala> implicit def stringToHogeable(src:String) = new { def hoge = 100 } stringToHogeable: (String)java.lang.Object{def hoge: Int} scala> "hogehoge".hoge res0: Int = 100
ここまで来るとかなり激しい感じがしますね。
上記の例では implicit defを解釈した時点で、戻り値となっている無名のクラスが hogeメソッドをもっていることはコンパイル時点で分かっているようです。これならどのimplicit def を呼び出すのかはScalaにはわかるわけですね。
そこで、こんな実験をしてみました
scala> implicit def hogeable(src:String) = new { | println("hogeable.new src="+src); | | def hoge = 100 | } hogeable: (String)java.lang.Object{def hoge: Int} scala> implicit def fugable(src:String) = new { | println("fugable.new src="+src); | | def fuga = 200 | } fugable: (String)java.lang.Object{def fuga: Int} scala> "abc".hoge hogeable.new src=abc hogeable.new src=abc res0: Int = 100 scala> "def".fuga fugable.new src=def fugable.new src=def res1: Int = 200 scala> "ghi".naiyo <console>:7: error: value naiyo is not a member of java.lang.String "ghi".naiyo ^
というわけで、
- implicit def後も存在しないメソッドを呼び出すようなケースでは、定義済みのimplicit defは試されない (これは当たり前というか実行効率上好都合ですね)
- 存在しないメソッドを呼ぶ際には、必要な変換に該当するimplicit defただ1つを特定して呼び出す
- 謎なのが、なぜ上記の例では new が2回走っているのかということ。つまり implicit def は二回評価されているのか...?
という感じになりました。
最後の「なぜ2回呼ばれているのか」はまだわかりません… 識者のコメントがいただけるとハッピーです!
ちなみに無名クラスでなければ一回のようです。
scala> class Test { | println(this); | | def piyo = 100 | } defined class Test scala> implicit def toTest(a:String) = new Test toTest: (String)Test scala> "hoge".piyo line5$object$$iw$$iw$Test@10c0fa7 res1: Int = 100