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

kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

何故implicit defが2回呼ばれるのか

Scala

(snip)
というわけで、

  • implicit def後も存在しないメソッドを呼び出すようなケースでは、定義済みのimplicit defは試されない (これは当たり前というか実行効率上好都合ですね)
  • 存在しないメソッドを呼ぶ際には、必要な変換に該当するimplicit defただ1つを特定して呼び出す
  • 謎なのが、なぜ上記の例では new が2回走っているのかということ。つまり implicit def は二回評価されているのか...?

という感じになりました。

最後の「なぜ2回呼ばれているのか」はまだわかりません… 識者のコメントがいただけるとハッピーです!
(snip)

http://d.hatena.ne.jp/tanigon/20090907#p2

この、「なぜ2回呼ばれているのか」は自分も不思議に思ったのでちょっと調査してみました。まずは、次のようなコードをscalacでコンパイルして実行してみます。

object ImplicitsTest {
  implicit def hogeable(src:String) = new {
    println("hogeable.new src="+src);
    def hoge = 100
  }
  def main(args: Array[String]) {
    hogeable("abc").hoge
  }
}
>scalac ImplicitsTest.scala

>scala ImplicitsTest
hogeable.new src=abc
hogeable.new src=abc

id:tanigonさんが試された場合と同様に、hogeableが2回実行されているように見えます。次に、このコードを逆アセンブルしてみます。

>javap -c ImplicitsTest$
(snip)
public void main(java.lang.String[]);
  Code:
   0:   aconst_null
   1:   astore_2
   2:   aload_0
   3:   ldc     #27; //String abc
   5:   invokevirtual   #31; //Method hogeable:(Ljava/lang/String;)Ljava/lang/Object;</strong>
   8:   invokevirtual   #35; //Method java/lang/Object.getClass:()Ljava/lang/Class;
   11:  invokestatic    #39; //Method reflMethod$Method1:(Ljava/lang/Class;)Ljava/lang/reflect/Method;
   14:  aload_0
   15:  ldc     #27; //String abc
   17:  invokevirtual   #31; //Method hogeable:(Ljava/lang/String;)Ljava/lang/Object;
   20:  iconst_0
   21:  anewarray       #20; //class java/lang/Object
   24:  invokevirtual   #45; //Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
   27:  astore_2
   28:  aload_2
   29:  checkcast       #47; //class java/lang/Integer
   32:  pop
   33:  return
   34:  astore_3
   35:  aload_3
   36:  invokevirtual   #53; //Method java/lang/reflect/InvocationTargetException.getCause:()Ljava/lang/Throwable;
   39:  athrow
  Exception table:
   from   to  target type
     2    28    34   Class java/lang/reflect/InvocationTargetException
(snip)

5:で始まる行と、17:で始まる行に注目してみると、確かにhogeableを2回呼ぶコードが生成されていることがわかります。問題は、何故このようなコードが生成されたかですが、hogeメソッドの呼び出しがリフレクション経由であることに注意してください。Scalaでは通常のメソッドの呼び出しはJavaと同様invokevirtualなどのJVMレベルのメソッド呼び出し命令が使われるはずなので、これはstructural type経由でメソッド呼び出しをしたためだと推測されます。実際、REPLでhogeableの型を調べてみると、

scala> implicit def hogeable(src:String) = new {
     |     println("hogeable.new src="+src);
     |     def hoge = 100
     |   }
hogeable: (String)java.lang.Object{def hoge: Int}

となり、hogeメソッドを持つstructural typeとして型が付いていることがわかります。ここまで来ると、この「2回呼ばれる」問題はimplicit defに特有の話ではなく、structural typeに対するメソッド呼び出し一般の問題ではないかと推測できます。というわけで、hogeableをimplicitでない普通の関数として定義した以下のプログラムをコンパイルして実行してみます(ついでにhogeableの戻り値の型も明示的に宣言するようにしてあります)。

object StructuralTypeTest {
  def hogeable(src:String): { def hoge: Int } = new {
    println("hogeable.new src="+src);
    def hoge = 100
  }
  def main(args: Array[String]) {
    hogeable("abc").hoge
  }
}

すると、次のような結果になり、同じくhogeableが2回実行されたことがわかります。

>scalac StructuralTypeTest.scala

>scala StructuralTypeTest
hogeable.new src=abc
hogeable.new src=abc

念のため、次のようなプログラムも試してみましたが、同様にdoNothingが2回実行されました。

object StructuralTypeTest2 {
  def doNothing(src: String): String = {
    println("src=" + src)
    src
  }
  def main(args: Array[String]) {
    //structural typeにキャストしてからメソッドを呼び出す
    (doNothing("abc"):{def charAt(index: Int): Char}).charAt(0)
  }
}

結論として、今回の問題は、structural typeに対するメソッド呼び出しにおいて、レシーバーの式が2回評価されるという実装に起因するものだと言えそうです。何故レシーバーの式を2回評価する実装になっているかは正直言ってわからないのですが、おそらく、その方がコード生成器の実装が楽だったからではないかと思います。というか、正直それ以外の合理的な理由が思いつきません。

追記:
この問題、implicit defについて言えば、単にimplicit defでは副作用起こすなということで済むし、実際、implicit defは副作用無しの関数であるべきだと思うんだけど、structural type返す関数も副作用持ってるとまずいことになる可能性があるってのは盲点な気がする。

さらに追記:
id:SiroKuroさん:

(snip)
ふつうにバグのような予感。本当なら下のようになるはずなのにね。
(snip)

http://d.hatena.ne.jp/SiroKuro/20090907/1252332858

@kinabaさん:

http://bit.ly/7doXz 単にバグじゃないですかねえ。exp.foo(params) を機械的に exp.getClass().getMethod("foo").invoke( exp, params ) に変換しちゃいましたテヘッ☆ていう感じに見える @kmizu

http://twitter.com/kinaba/statuses/3818049246

確かにバグの可能性はあるというか、むしろそっちの方が可能性高い気もしなくもないけど、このレベルのバグが未だに残ってるもんかなーという疑問があったのでした。Structural Typeが入ったのって、もう2年以上前の話ですし。ともあれ、はっきりしないのは何なので、MLに質問投げてみました。

さらに追記2:
MLに投げた質問の返答が返ってきました。それによると、Scala 2.8ではFIXされてるよーとのこと。結局、バグだったってことみたいですね。