kmizuの日記

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

Int#+の怪

Scalaでは32ビット符号付き整数を表すIntは特別な型ではなく、単にAnyのサブクラスであるAnyValのサブクラスであるIntクラスだという事になっているし、実際そのように扱われる。また、Intに対する+などの各種演算子も単なるIntクラスのメソッドであることになっているし、実際にそのように扱われる。では、Int型の+が単なるメソッドだとしたら、JavaのリフレクションAPIを経由して+メソッドを表すMethodオブジェクトを取得し、それを呼び出すことはできるのだろうか?

結論から言うと、無理である。まず、ScalaのInt型を使ったコードがクラスファイルにコンパイルされる際はJVMレベルでは可能な限りJVMのプリミティブ型であるint型を利用し、コレクションにInt型の値が格納される場合など、それが不可能な場合はjava.lang.Integer型に自動的にboxingされるようになっている。ここで、java.lang.Integer型に+メソッドなどというものがあれば良いのだが、Javaプログラマならわかるように、当然そんなメソッドは無いので、リフレクションAPIを経由して+メソッドを表すMethodオブジェクトを取得することはできない、ということになる。

念のため、REPLで

scala> 0.asInstanceOf[AnyRef].getClass.getMethods.filter(_.getName == "$plus")

のように入力して、$plusメソッド(Scalaの+メソッドはJVMレベルでは$plusという名前にmanglingされる)を探してみたが、当然、

res4: Array[java.lang.reflect.Method] = Array()

となり、そのようなメソッドは見つからなかった。というわけで、Int型の$plusメソッドをリフレクション経由で呼び出すことは無理なわけだが、だとすると一つ疑問がある。Scalaにはstructural typeという機能があり、「あるシグニチャを持ったメソッドだけを持った型」のようなものを表現できる。これを使うと、以下のようなコードを評価することができる。Any{ def +(x: Int): Int}というコードで、Anyのサブタイプでかつ「Int型を引数として受け取って、Int型を返す+メソッド」を持った任意のオブジェクトを受け取れるような型を表現している。

scala> {val i: Any{def +(x: Int): Int} = 1; i + 1}
res5: Int = 2

このstructural typeという機能、知っている人ならわかると思うが、リフレクションAPIを使ってMethodオブジェクトを取得してそれを呼び出すことで実現されている。しかし、Int型に対して+メソッドを表すMethodオブジェクトを取得することはできないはず。一体どのようにして実現しているのだろうか…というわけで、

object I {
  def main(args: Array[String]) {
    val x: Any{ def +(i: Int): Int } = 10
    println(x + 10)
  }
}

というコードをscalacでコンパイルして、javap -cで逆アセンブルしてみた。以下が逆アセンブル結果である。

Compiled from "I.scala"
public final class I$ extends java.lang.Object implements scala.ScalaObject{
public static final I$ MODULE$;

public static {};
  Code:
   0:   new     #10; //class I$
   3:   invokespecial   #13; //Method "<init>":()V
   6:   return

public void main(java.lang.String[]);
  Code:
   0:   ldc     #41; //int 10
   2:   invokestatic    #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I
)Ljava/lang/Integer;
   5:   astore_2
   6:   getstatic       #52; //Field scala/Predef$.MODULE$:Lscala/Predef$;
   9:   aload_2
   10:  astore_3
   11:  aload_2
   12:  instanceof      #54; //class java/lang/Number
   15:  ifne    25
   18:  aload_2
   19:  instanceof      #56; //class java/lang/Character
   22:  ifeq    40
   25:  aload_3
   26:  ldc     #41; //int 10
   28:  invokestatic    #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I
)Ljava/lang/Integer;
   31:  invokestatic    #60; //Method scala/runtime/BoxesRunTime.add:(Ljava/lang
/Object;Ljava/lang/Object;)Ljava/lang/Object;
   34:  checkcast       #22; //class java/lang/Integer
   37:  goto    73
   40:  aconst_null
   41:  astore  4
   43:  aload_3
   44:  invokevirtual   #64; //Method java/lang/Object.getClass:()Ljava/lang/Cla
ss;
   47:  invokestatic    #68; //Method reflMethod$Method1:(Ljava/lang/Class;)Ljav
a/lang/reflect/Method;
   50:  aload_3
   51:  iconst_1
   52:  anewarray       #35; //class java/lang/Object
   55:  dup
   56:  iconst_0
   57:  ldc     #41; //int 10
   59:  invokestatic    #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I
)Ljava/lang/Integer;
   62:  aastore
   63:  invokevirtual   #74; //Method java/lang/reflect/Method.invoke:(Ljava/lan
g/Object;[Ljava/lang/Object;)Ljava/lang/Object;
   66:  astore  4
   68:  aload   4
   70:  checkcast       #22; //class java/lang/Integer
   73:  invokevirtual   #78; //Method scala/Predef$.println:(Ljava/lang/Object;)
V
   76:  return
   77:  astore  5
   79:  aload   5
   81:  invokevirtual   #84; //Method java/lang/reflect/InvocationTargetExceptio
n.getCause:()Ljava/lang/Throwable;
   84:  athrow
  Exception table:
   from   to  target type
    43    68    77   Class java/lang/reflect/InvocationTargetException


public static java.lang.reflect.Method reflMethod$Method1(java.lang.Class);
  Code:
   0:   getstatic       #33; //Field reflPoly$Cache1:Lscala/runtime/MethodCache;

   3:   aload_0
   4:   invokevirtual   #97; //Method scala/runtime/MethodCache.find:(Ljava/lang
/Class;)Ljava/lang/reflect/Method;
   7:   astore_1
   8:   aload_1
   9:   ifnull  14
   12:  aload_1
   13:  areturn
   14:  aload_0
   15:  ldc     #99; //String $plus
   17:  getstatic       #28; //Field reflParams$Cache1:[Ljava/lang/Class;
   20:  invokevirtual   #103; //Method java/lang/Class.getMethod:(Ljava/lang/Str
ing;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
   23:  astore_1
   24:  getstatic       #33; //Field reflPoly$Cache1:Lscala/runtime/MethodCache;

   27:  aload_0
   28:  aload_1
   29:  invokevirtual   #106; //Method scala/runtime/MethodCache.add:(Ljava/lang
/Class;Ljava/lang/reflect/Method;)Lscala/runtime/MethodCache;
   32:  putstatic       #33; //Field reflPoly$Cache1:Lscala/runtime/MethodCache;

   35:  aload_1
   36:  areturn

}

若干長いが、重要なのは以下の部分だ。

   12:  instanceof      #54; //class java/lang/Number
   15:  ifne    25
   18:  aload_2
   19:  instanceof      #56; //class java/lang/Character
   22:  ifeq    40
   25:  aload_3
   26:  ldc     #41; //int 10
   28:  invokestatic    #47; //Method scala/runtime/BoxesRunTime.boxToInteger:(I
)Ljava/lang/Integer;
   31:  invokestatic    #60; //Method scala/runtime/BoxesRunTime.add:(Ljava/lang
/Object;Ljava/lang/Object;)Ljava/lang/Object;
   34:  checkcast       #22; //class java/lang/Integer
   37:  goto    73

このコードを見ると、レシーバがjava.lang.Numberインスタンスまたjava.lang.Characterインスタンスかをチェックして、もしそうならばBoxesRuntime.add(Object,Object)という専用のメソッドを呼び出していることがわかる。というわけで、structural typeの実現にあたって、プリミティブ型のラッパークラスに関しては特別扱いするようなコードをscalacが吐いているのであった。ちゃんちゃん。