メソッドの実装クラスを探す
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) }
パッケージ無しクラスだととかになってしまうので、微調整がいるかも。
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を見て、順番に探せば良いけど。