Custom CSS

Tuesday, June 10, 2008

Scala: Querying an object's fields and methods with reflection

Sometimes you might want to know what the methods and fields of an object are while playing around in the Scala interpreter. Rather than holding off on what you were doing and looking through the API docs (or wishing there were a Scoogle), you can query an object for its methods dynamically using Java reflection. It won't be as painful as doing it in Java, as you can use implicit definitions to get the methods of an object just by typing object.methods (a la Ruby).

Here's what it'll look like:

scala> // This gives you a list you can work with if you want
scala> 1.methods
res8: List[String] = List(hashCode, reverseBytes(int), compareTo(Integer), 
compareTo(Object), equals(Object), toString(int,int), toString(int), ...

scala> // This prints out the methods so there is no truncation
scala> // I just truncated the list to save space.
scala> 1.methods_
hashCode, reverseBytes(int), compareTo(Integer), compareTo(Object), 
equals(Object), toString(int,int), toString(int), toString, toHexString(int), 
decode(String), getChars(int,int,char[]), valueOf(String,int), ...

scala> // Again, I truncated to save space
scala> 1.methods__

The code, as it turned out, introduces a lot of Scala concepts which might overwhelm a newbie (and most of the time, newbies - like me - are the ones who would want to do this in the first place), so I've posted the code at the end, which you can just put into a file and type import ScalaReflection._ to make it work. If you want to know about how this all actually works, read on.

First of all, implicit definitions allow you to define a coercion that the compiler should apply when convenient. As in Java, we can't just add methods to an existing class (in this case, the class Any), however, we can define new classes with those methods, and coerce when necessary. For example, when 1.methods is called, Scala will see that method methods doesn't exist, and coerce into an AnyExtras which declares methods.

In the linked code, you'll see that AnyRef and AnyVal are treated differently. This is because Scala uses the underlying system's primitives in the case of value types like ints, doubles, chars, etc. In Java's case, ints are coerced into java.lang.Integer; however, Scala defines scala.runtime.RichInt for cases when Integer just won't cut it. This is unfortunate for our case because, silent conversions to both classes take place when using integer literals and the conversions can fail if there is ambiguity. The ambiguity here is the getClass method, which is defined in both java.lang.Integer and scala.runtime.RichInt; when getClass is called on a scala.Int, Scala won't know which conversion to apply. Of course, we want java.lang.Integer, and in general, we'll want the Java wrapper. So prior to querying the object for methods, we wrap object appropriately (if necessary) and cast it as an AnyRef if not.

Also notice the usage of NameTransformer from the scala-compiler artifact. Java (and probably .NET languages) will not allow methods with most non-alphanumerics, but Scala is much more liberal. As a result, something like def ==(...) is encoded as def $eq$eq(...), and rather than seeing that in your output, the transformer can decode $eq$eq back to ==. Other than that, we just get the methods of the object, strip away the scope qualifiers and the throws clauses, and print out the names as Strings.

Here's the code:

import scala.Console._
object ScalaReflection {
  implicit def any2anyExtras(x: Any) = new AnyExtras(x)
class AnyExtras(x: Any) {
  def methods_ = println(methods.reduceLeft[String](_ + ", " + _))
  def methods__ = methods.foreach(println _)
  def fields_ = println(fields.reduceLeft[String](_ + ", " + _))
  def fields__ = fields.foreach(println _)
  def methods = wrapped.getClass
      .map(m => decode(m.toString
                        .replaceFirst("\\).*", ")")
                        .replaceAll("[^(]+\\.", "")
                        .replace("()", "")))
  def fields = wrapped.getClass
      .map(m => decode(m.toString.replaceFirst("^.*\\.", "")))
  private def wrapped: AnyRef = x match {
    case x: Byte => byte2Byte(x)
    case x: Short => short2Short(x)
    case x: Char => char2Character(x)
    case x: Int => int2Integer(x)
    case x: Long => long2Long(x)
    case x: Float => float2Float(x)
    case x: Double => double2Double(x)
    case x: Boolean => boolean2Boolean(x)
    case _ => x.asInstanceOf[AnyRef]