views:

532

answers:

4

To make debug-time introspection into classes easy, I'd like to make a generic toString method in the base class for the objects in question. As it's not performance critical code, I'd like to use Reflection to print out field name/value pairs ("x=1, y=2" etc).

Is there an easy way to do this? I tried several potential solutions, and ran up against security access issues, etc.

To be clear, the toString() method in the base class should reflectively iterate over public vals in any classes that inherit from it, as well as any traits that are mixed in.

+2  A: 

Example:

override def toString() = {
  getClass().getDeclaredFields().map { field:Field => 
    field.setAccessible(true)
    field.getName() + ": " + field.getType() + " = " + field.get(this).toString()
  }.deepMkString("\n")
}

Uses Java Reflection API, so don't forget to import java.lang.reflect._

Also, you may need to catch IllegalAccessException on the field.get(this) calls in some scenarios, but this is just meant as a starting point.

Lytol
I've been using something similar to this already, but I get a lot of access exceptions even for things that are public vals. I'm confused about this, as I expected that if I had code like this:class Foo { val bar = 3}that if I called, from another class, Foo.getDeclaredField("bar"), the resulting field object would have the modifier "public". I have been using java.lang.reflect.Modifier to decode the modifier fields, and it does not work.This is in 2.7.7, so I'm not sure if it differs from 2.8 in this regard.
Justin W
+2  A: 

Scala doesn't generate any public fields. They're all going to be private. The accessor methods are what will be public, reflect upon those. Given a class like:

class A {
  var x = 5
}

The generated bytecode looks like:

private int x;
public void x_$eq(int);
public int x();
extempore
I believe one time long ago I used to know this. Thanks!
Justin W
+1  A: 
import util._                 // For Scala 2.8.x NameTransformer
import scala.tools.nsc.util._ // For Scala 2.7.x NameTransformer

/**
 * Repeatedly run `f` until it returns None, and assemble results in a Stream.
 */
def unfold[A](a: A, f: A => Option[A]): Stream[A] = {
  Stream.cons(a, f(a).map(unfold(_, f)).getOrElse(Stream.empty))
}

def get[T](f: java.lang.reflect.Field, a: AnyRef): T = {
  f.setAccessible(true)
  f.get(a).asInstanceOf[T]
}

/**
 * @return None if t is null, Some(t) otherwise.
 */
def optNull[T <: AnyRef](t: T): Option[T] = if (t eq null) None else Some(t)

/**
 * @return a Stream starting with the class c and continuing with its superclasses.
 */
def classAndSuperClasses(c: Class[_]): Stream[Class[_]] = unfold[Class[_]](c, (c) => optNull(c.getSuperclass))

def showReflect(a: AnyRef): String = {
  val fields = classAndSuperClasses(a.getClass).flatMap(_.getDeclaredFields).filter(!_.isSynthetic)
  fields.map((f) => NameTransformer.decode(f.getName) + "=" + get(f, a)).mkString(",")
}

// TEST
trait T {
  val t1 = "t1"
}

class Base(val foo: String, val ?? : Int) {
}

class Derived(val d: Int) extends Base("foo", 1) with T

assert(showReflect(new Derived(1)) == "t1=t1,d=1,??=1,foo=foo")
retronym
I should have mentioned that I'm stuck with 2.7.7 for now. I wasn't sure about some of the new 2.8 constructs you used in your code, and ended up with a null pointer exception. This looks like a fairly clean way to do it though, so the answer goes to you!
Justin W
I've edited the example to be compatible with 2.7.7.
retronym
+1  A: 

Are you aware the Scala case classes get these compiler-generated methods:

  • toString(): String
  • equals(other: Any): Boolean
  • hashCode: Int

They also get companion objects for "new-less" constructors and pattern matching.

The generated toString() is pretty much like the one you describe.

Randall Schulz
Yes, case classes are magic. Unfortunately I am not using them here, for various reasons. I do use all of the features you listed in other places, however.I'll be sure my toString trait can handle case classes in the future.Also, good seeing you on IRC last night, small world.
Justin W