Reflection on a Scala case class
Asked Answered
T

4

12

I'm trying to write a trait (in Scala 2.8) that can be mixed in to a case class, allowing its fields to be inspected at runtime, for a particular debugging purpose. I want to get them back in the order that they were declared in the source file, and I'd like to omit any other fields inside the case class. For example:

trait CaseClassReflector extends Product {

  def getFields: List[(String, Any)] = {
    var fieldValueToName: Map[Any, String] = Map()
    for (field <- getClass.getDeclaredFields) {
      field.setAccessible(true)
      fieldValueToName += (field.get(this) -> field.getName) 
    }
    productIterator.toList map { value => fieldValueToName(value) -> value }
  }

}

case class Colour(red: Int, green: Int, blue: Int) extends CaseClassReflector {
  val other: Int = 42
}

scala> val c = Colour(234, 123, 23)
c: Colour = Colour(234,123,23)

scala> val fields = c.getFields    
fields: List[(String, Any)] = List((red,234), (green,123), (blue,23))

The above implementation is clearly flawed because it guesses the relationship between a field's position in the Product and its name by equality of the value on those field, so that the following, say, will not work:

Colour(0, 0, 0).getFields

Is there any way this can be implemented?

Ticktock answered 8/2, 2010 at 19:27 Comment(2)
There's a bug in your code. Values are not unique, and therefore you're going to overwrite values with (field.get(this) -> field.getName) when move than one field has a given name. See a rewritten version of your code below.Caird
@SagieDavidovich Indeed, as noted, "the above implementation is clearly flawed"Ticktock
C
7

In every example I've seen the fields are in reverse order: the last item in the getFields array is the first one listed in the case class. If you use case classes "nicely", then you should just be able to map productElement(n) onto getDeclaredFields()( getDeclaredFields.length-n-1).

But this is rather dangerous, as I don't know of anything in the spec that insists that it must be that way, and if you override a val in the case class, it won't even appear in getDeclaredFields (it'll appear in the fields of that superclass).

You might change your code to assume things are this way, but check that the getter method with that name and the productIterator return the same value and throw an exception if they don't (which means that you don't actually know what corresponds to what).

Cholinesterase answered 8/2, 2010 at 20:35 Comment(4)
I am facing the same problem as Matt R was facing. Being a relative noob in Scala, can you please explain your answer a little bit more. That would be quite helpful. Thanks!Yellowbird
@Yellowbird - Actually, I think that you are more likely to get in trouble than to come up with a solution to your problem unless you can use my vague hints above to craft your own solution. As I said, "this is rather dangerous". It's your responsibility to be able to anticipate and avoid the problems, which may require transitioning from "relative noob" to "not" at least in this aspect. Play around with reflection and sample case classes in the REPL and see if you can figure it out!Cholinesterase
getClass.getDeclaredFields.map(_.getName).zip(productIterator.toList).toMapConsolata
I found @ÁkosVandra comment used in my code base. It works on Scala 2.11 but is broken on Scala 2.12 if a case class contains a lazy val so I don't recommend using it going forwards.Rockafellow
M
10

Look in trunk and you'll find this. Listen to the comment, this is not supported: but since I also needed those names...

/** private[scala] so nobody gets the idea this is a supported interface.
 */
private[scala] def caseParamNames(path: String): Option[List[String]] = {
  val (outer, inner) = (path indexOf '$') match {
    case -1   => (path, "")
    case x    => (path take x, path drop (x + 1))
  }

  for {
    clazz <- getSystemLoader.tryToLoadClass[AnyRef](outer)
    ssig <- ScalaSigParser.parse(clazz)
  }
  yield {
    val f: PartialFunction[Symbol, List[String]] =
      if (inner.isEmpty) {
        case x: MethodSymbol if x.isCaseAccessor && (x.name endsWith " ") => List(x.name dropRight 1)
      }
      else {
        case x: ClassSymbol if x.name == inner  =>
          val xs = x.children filter (child => child.isCaseAccessor && (child.name endsWith " "))
          xs.toList map (_.name dropRight 1)
      }

    (ssig.symbols partialMap f).flatten toList
  }
}
Madelina answered 11/2, 2010 at 14:22 Comment(0)
C
10

Here's a short and working version, based on the example above

  trait CaseClassReflector extends Product {
    def getFields = getClass.getDeclaredFields.map(field => {
      field setAccessible true
      field.getName -> field.get(this)
    })
  }
Caird answered 27/3, 2013 at 10:31 Comment(1)
I tried this and it worked, at least for a simple case class.Carious
C
7

In every example I've seen the fields are in reverse order: the last item in the getFields array is the first one listed in the case class. If you use case classes "nicely", then you should just be able to map productElement(n) onto getDeclaredFields()( getDeclaredFields.length-n-1).

But this is rather dangerous, as I don't know of anything in the spec that insists that it must be that way, and if you override a val in the case class, it won't even appear in getDeclaredFields (it'll appear in the fields of that superclass).

You might change your code to assume things are this way, but check that the getter method with that name and the productIterator return the same value and throw an exception if they don't (which means that you don't actually know what corresponds to what).

Cholinesterase answered 8/2, 2010 at 20:35 Comment(4)
I am facing the same problem as Matt R was facing. Being a relative noob in Scala, can you please explain your answer a little bit more. That would be quite helpful. Thanks!Yellowbird
@Yellowbird - Actually, I think that you are more likely to get in trouble than to come up with a solution to your problem unless you can use my vague hints above to craft your own solution. As I said, "this is rather dangerous". It's your responsibility to be able to anticipate and avoid the problems, which may require transitioning from "relative noob" to "not" at least in this aspect. Play around with reflection and sample case classes in the REPL and see if you can figure it out!Cholinesterase
getClass.getDeclaredFields.map(_.getName).zip(productIterator.toList).toMapConsolata
I found @ÁkosVandra comment used in my code base. It works on Scala 2.11 but is broken on Scala 2.12 if a case class contains a lazy val so I don't recommend using it going forwards.Rockafellow
P
4

You can also use the ProductCompletion from the interpreter package to get to attribute names and values of case classes:

import tools.nsc.interpreter.ProductCompletion

// get attribute names
new ProductCompletion(Colour(1, 2, 3)).caseNames
// returns: List(red, green, blue)

// get attribute values
new ProductCompletion(Colour(1, 2, 3)).caseFields

Edit: hints by roland and virtualeyes

It is necessary to include the scalap library which is part of the scala-lang collection.

Thanks for your hints, roland and virtualeyes.

Pease answered 6/9, 2011 at 13:4 Comment(3)
Note that the call to caseNames only works if scalap (scala-lang.org/node/292) can be found on the classpath. Otherwise an empty list will be returned (when using Scala 2.9.1).Hemiplegia
+1 @roland, it's true what you say. Given that scalap download does not exactly leap out in a google search, here it is for 2.9.1: oss.sonatype.org/content/groups/scala-tools/org/scala-lang/…Swag
note the catch 22 of needing a case class instance prior to being able to reflect on it. Hopefully 2.10 will bring the goods so to speak in this regard, hands are tied in 2.9, working with black box case classes is a PITA, wind up typing out domain model, ORM mapping, and validation in triplicate, wtf...Swag

© 2022 - 2024 — McMap. All rights reserved.