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?

oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
Matt R
  • 9,892
  • 10
  • 50
  • 83
  • 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. – Sagie Davidovich Mar 27 '13 at 10:33
  • @SagieDavidovich Indeed, as noted, "the above implementation is clearly flawed" – Matt R Mar 27 '13 at 10:46

4 Answers4

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)
    })
  }
Sagie Davidovich
  • 578
  • 4
  • 14
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
  }
}
psp
  • 12,138
  • 1
  • 41
  • 51
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).

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
  • 1
    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! – Core_Dumped Dec 06 '13 at 15:20
  • 2
    @Core_Dumped - 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! – Rex Kerr Dec 15 '13 at 21:31
  • getClass.getDeclaredFields.map(_.getName).zip(productIterator.toList).toMap – Ákos Vandra-Meyer Apr 21 '16 at 11:58
  • 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. – Philluminati Jun 21 '18 at 15:48
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.

Stefan Endrullis
  • 4,150
  • 2
  • 32
  • 45
  • 1
    Note that the call to `caseNames` only works if scalap (http://www.scala-lang.org/node/292) can be found on the classpath. Otherwise an empty list will be returned (when using Scala 2.9.1). – Roland Ewald Feb 03 '12 at 14:56
  • 1
    +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: https://oss.sonatype.org/content/groups/scala-tools/org/scala-lang/scalap/2.9.1/ – virtualeyes Feb 12 '12 at 18:18
  • 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... – virtualeyes Feb 20 '12 at 18:34