既存クラスを拡張(したように見せる)ひとつの方法

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