メソッドの実装クラスを探す

Scalaでコード書いていて、標準クラスの実装方法を見てみたくなることがある。※例えば、Listクラスのproduct()メソッドの実装など


ソースコードを見るのに、メソッドがどこに実装されているかを知る必要があるんだけど、Javaのインターフェスと違ってScalaはTraitに実装を持てるので、複数のTraitを継承している場合に全てのTraitを遡って調べる必要がある。ちょっと面倒くさい。*1


そこで、クラスとメソッド名を渡すと、実装されているクラスを探すプログラムを書いてみた。ScalaSigParserというのを使ったよ。


参考ページ:

実装

import tools.scalap.scalax.rules.scalasig._

object MethodFinderSample {

    def getConcreteMethods(c: Class[_], name: String): Seq[String] =
        for (sig <-ScalaSigParser.parse(c).toSeq;
             sym <- sig.topLevelClasses;
             t <- sym.children if (t.isMethod && !t.isDeferred && t.name == name))
            yield c.getName + "#" + t.name

    def getSupers(c: Class[_]) =
        if (c.getSuperclass != null)
            c.getSuperclass :: c.getInterfaces.toList
        else 
            c.getInterfaces.toSeq

    implicit def classToTraversable(c: Class[_]): Traversable[Class[_]] = new Traversable[Class[_]] {
        def foreach[U](f: Class[_] => U): Unit = {
            f(c)
            getSupers(c) foreach (_ foreach f)
        }
    }

    def findConcreteMethods(c: Class[_], name: String): Traversable[String] =
        c flatMap (getConcreteMethods(_ , name)) toSet

    def main(args: Array[String]) {
        val methods = findConcreteMethods(List(1).getClass, "product")
        methods foreach println // "scala.collection.TraversableOnce#product"
    }
}

引数の数や型は見ていない。名前のみ。
実メソッドをオーバーライドしている場合、両方のクラスのものが表示される。

説明1. クラスの実装メソッドを確認する

次のようなクラスがあるとする。

trait A {
    def hoge = "hoge"
}
trait B {
    def fuga = "fuga"
}
class C extends A with B {
}

hoge()はAに、fuga()はBに実装されている。
リフレクションを使って調べると、次のようにどちらもCに実装されているように見えてしまう。

scala> classOf[C].getMethods foreach (c => printf("%s#%s\n",c.getDeclaringClass.getName, c.getName))
C#hoge
C#fuga
...

JVMの世界では多重継承を許していないので、コンパイル時にクラスCに委譲用のメソッドが追加されてしまい、リフレクションを使って見えるのはそのメソッドになってしまっているようだ。


ScalaSigParserというのを使って、コンパイル後のクラスからScala的な情報を取得出来るらしい。


sbtのビルドファイルに追加(Scalaのバージョンは2.9.1を使用している)

    libraryDependencies += "org.scala-lang" % "scalap" % "2.9.1"


クラスのシグネチャを取得してみる。

scala> import tools.scalap.scalax.rules.scalasig._
import tools.scalap.scalax.rules.scalasig._

scala> val sig = ScalaSigParser.parse(List(1).getClass).get
sig: scala.tools.scalap.scalax.rules.scalasig.ScalaSig = 
ScalaSig version 5.0
0:	ClassSymbol($colon$colon, owner=scala.collection.immutable, flags=42, info=9 ,None)
1:	$colon$colon
2:	scala.collection.immutable
3:	immutable
4:	scala.collection
5:	collection
6:	scala
7:	scala
8:	NoSymbol
9:	PolyType(ClassInfoType(ClassSymbol($colon$colon, owner=scala.collection.immutable, flags=42, info=9 ,None),List(TypeRefType(ThisType(scala.collection.immutable),scala.collection.immutable.List,List(TypeRefType(NoPrefixType,TypeSymbol(B, owner=0, flags=2100, info=19 ),List()))), TypeRefType(ThisType(scala),scala.ScalaObject,List()), TypeRefType(ThisType(scala),scala.Product,List()), TypeRefType(ThisType(scala),scala.Serializable,List()))),List(TypeSymbol(B, owner=0, flags=2100, info=19 )))
10:	ClassIn...

クラスのシンボル情報は、ScalaSigのtopLevelClassesで取得出来る

scala> val sym = sig.topLevelClasses head
sym: scala.tools.scalap.scalax.rules.scalasig.ClassSymbol = ClassSymbol($colon$colon, owner=scala.collection.immutable, flags=42, info=9 ,None)

メソッドの一覧を取得してみる

scala> sym.children filter (_.isMethod) foreach println
MethodSymbol(hd, owner=0, flags=28000204, info=38 ,None)
MethodSymbol(hd_$eq, owner=0, flags=28000204, info=41 ,None)
MethodSymbol(tl, owner=0, flags=28000200, info=51, privateWithin=scala,None)
MethodSymbol(tl_$eq, owner=0, flags=28000200, info=54, privateWithin=scala,None)
MethodSymbol(<init>, owner=0, flags=200, info=60 ,None)
MethodSymbol(head, owner=0, flags=220, info=38 ,None)
MethodSymbol(tail, owner=0, flags=220, info=51 ,None)
MethodSymbol(isEmpty, owner=0, flags=220, info=70 ,None)
MethodSymbol(writeObject, owner=0, flags=204, info=76 ,None)
MethodSymbol(readObject, owner=0, flags=204, info=89 ,None)
MethodSymbol(copy, owner=0, flags=200200, info=97 ,None)
MethodSymbol(copy$default$1, owner=0, flags=2200200, info=107 ,None)
MethodSymbol(copy$default$2, owner=0, flags=2200200, info=122 ,None)
MethodSymbol(hd$1, owner=0, flags=1200200, info=38 ,None)
MethodSymbol(tl$1, owner=0, flags=1200200, info=51 ,None)
MethodSymbol(productPrefix, owner=0, flags=220, info=132 ,None)
MethodSymbol(productArity, owner=0, flags=220, info=141 ,None)
MethodSymbol(productElement, owner=0, flags=220, info=147 ,None)

どうやらこのクラス自身に実装されたメソッドがとれるようだ。


これを踏まえて、そのクラスに実メソッドが定義されているかどうかを調べる関数を書いてみる。

    def getConcreteMethods(c: Class[_], name: String): Seq[String] =
        for (sig <-ScalaSigParser.parse(c).toSeq;
             sym <- sig.topLevelClasses;
             t <- sym.children if (t.isMethod && !t.isDeferred && t.name == name))
            yield c.getName + "#" + t.name

例のクラスに使ってみる

scala> getConcreteMethods(classOf[C], "hoge")
res13: Seq[String] = List()

scala> getConcreteMethods(classOf[A], "hoge")
res12: Seq[String] = List(A#hoge)

CではなくAに定義されていると分かる。

説明2. 継承関係を取得する

Traitも含めて、継承関係はClass#getSuperclassとClass#getInterfacesでOKみたい。
traitは同名のInterfaceに見える。

scala> classOf[List[_]].getInterfaces foreach println
interface scala.collection.immutable.LinearSeq
interface scala.Product
interface scala.collection.generic.GenericTraversableTemplate
interface scala.collection.LinearSeqOptimized
interface scala.ScalaObject

スーパークラスとインタフェースを取れるメソッドを定義。

    def getSupers(c: Class[_]) =
        if (c.getSuperclass != null)
            c.getSuperclass :: c.getInterfaces.toList
        else 
            c.getInterfaces.toSeq

クラスを再帰的にトラバースするにはどうしたら良いか。折角なのでClassをTraversableにしてみた。

    implicit def classToTraversable(c: Class[_]): Traversable[Class[_]] = new Traversable[Class[_]] {
        def foreach[U](f: Class[_] => U): Unit = {
            f(c)
            getSupers(c) foreach (_ foreach f)
        }
    }

これで、クラスに向かってflatMapを使うと全ての先祖クラスまでトラバースできる。

    def findConcreteMethods(c: Class[_], name:String): Traversable[String] =
        c flatMap (getConcreteMethods(_ , name)) toSet

共通の親クラスを持っている場合があるのでtoSetでユニークにしている。
※本当は探す時に省けば効率的だけど。


例のクラスに使ってみる。

scala> findConcreteMethods(classOf[C], "hoge") foreach println
A#hoge

scala> findConcreteMethods(classOf[C], "fuga") foreach println
B#fuga

ちゃんとそれぞれのTraitの名前が取れている。

蛇足: ClassSymbolから継承関係を取得する

一応ScalaSig上でも継承関係が取得出来そう。
まず、親クラス・インタフェースはClassSymbol直下のClassInfoTypeか、PolyTypeの下のClassInfoTypeに入っているっぽい。

    def getSuperclasses(c: ClassSymbol): Seq[Class[_]] = {
        val typeRefs = c.infoType match {
            case PolyType(ClassInfoType(symbol, typeRefs), symbols) => typeRefs
            case ClassInfoType(symbol, typeRefs) => typeRefs
            case _ => Nil
        }

typeRefsには親クラス・インタフェース毎にTypeRefTypeが入っているので、その中のExternalSymbolからFQCNを作ってClass.forName()でクラスを得る。

        for (TypeRefType(_, externalSymbol, _) <- typeRefs)
            yield Class.forName(externalSymbol.path)
    }

パッケージ無しクラスだと.Aとかになってしまうので、微調整がいるかも。

    def getSuperclasses(c: ClassSymbol): Seq[Class[_]] = {
        val typeRefs = c.infoType match {
            case PolyType(ClassInfoType(symbol, typeRefs), symbols) => typeRefs
            case ClassInfoType(symbol, typeRefs) => typeRefs
            case _ => Nil
        }
        def getPath(parent: Option[Symbol], name: String): String= parent match {
            case Some(s:Symbol) if !(s.name == "<empty>") => s.path + "." + name
            case _ => name
        }
        for (TypeRefType(_, ExternalSymbol(name, parent,  _), _) <- typeRefs)
            yield Class.forName(getPath(parent, name))
    }

*1:scaladocがあれば、Definition Classesを見て、順番に探せば良いけど。