Scala: reflection and case classes

The following code succeeds, but is there a better way to do the same? Perhaps something specific for class classes? In the following code, for each field of type String in my class of simple cases, the code goes through my list of instances of this class case and finds the length of the longest string of this field.

case class CrmContractorRow( id: Long, bankCharges: String, overTime: String, name$id: Long, mgmtFee: String, contractDetails$id: Long, email: String, copyOfVisa: String) object Go { def main(args: Array[String]) { val a = CrmContractorRow(1,"1","1",4444,"1",1,"1","1") val b = CrmContractorRow(22,"22","22",22,"55555",22,"nine long","22") val c = CrmContractorRow(333,"333","333",333,"333",333,"333","333") val rows = List(a,b,c) c.getClass.getDeclaredFields.filter(p => p.getType == classOf[String]).foreach{f => f.setAccessible(true) println(f.getName + ": " + rows.map(row => f.get(row).asInstanceOf[String]).maxBy(_.length)) } } } 

Result:

 bankCharges: 3 overTime: 3 mgmtFee: 5 email: 9 copyOfVisa: 3 
+7
reflection scala case-class shapeless scala-reflect
source share
4 answers

If you want to do this with Shapeless, I would strongly suggest defining a custom type class that handles the complex part and allows you to keep this material separate from the rest of your logic.

In this case, it seems that the hard part of what you are specifically trying to do is get a mapping from field names to string lengths for all members of the String class of the case class. Here is a type class that does this:

 import shapeless._, shapeless.labelled.FieldType trait StringFieldLengths[A] { def apply(a: A): Map[String, Int] } object StringFieldLengths extends LowPriorityStringFieldLengths { implicit val hnilInstance: StringFieldLengths[HNil] = new StringFieldLengths[HNil] { def apply(a: HNil): Map[String, Int] = Map.empty } implicit def caseClassInstance[A, R <: HList](implicit gen: LabelledGeneric.Aux[A, R], sfl: StringFieldLengths[R] ): StringFieldLengths[A] = new StringFieldLengths[A] { def apply(a: A): Map[String, Int] = sfl(gen.to(a)) } implicit def hconsStringInstance[K <: Symbol, T <: HList](implicit sfl: StringFieldLengths[T], key: Witness.Aux[K] ): StringFieldLengths[FieldType[K, String] :: T] = new StringFieldLengths[FieldType[K, String] :: T] { def apply(a: FieldType[K, String] :: T): Map[String, Int] = sfl(a.tail).updated(key.value.name, a.head.length) } } sealed class LowPriorityStringFieldLengths { implicit def hconsInstance[K, V, T <: HList](implicit sfl: StringFieldLengths[T] ): StringFieldLengths[FieldType[K, V] :: T] = new StringFieldLengths[FieldType[K, V] :: T] { def apply(a: FieldType[K, V] :: T): Map[String, Int] = sfl(a.tail) } } 

It looks complicated, but as soon as you start working with Shapeless, you will learn to write such things in a dream.

Now you can write the logic of your operation in a relatively simple way:

 def maxStringLengths[A: StringFieldLengths](as: List[A]): Map[String, Int] = as.map(implicitly[StringFieldLengths[A]].apply).foldLeft( Map.empty[String, Int] ) { case (x, y) => x.foldLeft(y) { case (acc, (k, v)) => acc.updated(k, acc.get(k).fold(v)(accV => math.max(accV, v))) } } 

And then (given rows , as defined in the question):

 scala> maxStringLengths(rows).foreach(println) (bankCharges,3) (overTime,3) (mgmtFee,5) (email,9) (copyOfVisa,3) 

This will work for absolutely any class of case.

If this is a one-time thing, you can use reflection at runtime, or you can use the Poly1 approach in Giovanni Caporaretti's answer - it is less general and mixes the different parts of the solution this way I don't prefer, but it should work fine. If this is what you do a lot, I would suggest the approach that I have given here.

+10
source share

If you want to use formless to get the string fields of the case class and avoid reflection, you can do something like this:

 import shapeless._ import labelled._ trait lowerPriorityfilterStrings extends Poly2 { implicit def default[A] = at[Vector[(String, String)], A] { case (acc, _) => acc } } object filterStrings extends lowerPriorityfilterStrings { implicit def caseString[K <: Symbol](implicit w: Witness.Aux[K]) = at[Vector[(String, String)], FieldType[K, String]] { case (acc, x) => acc :+ (w.value.name -> x) } } val gen = LabelledGeneric[CrmContractorRow] val a = CrmContractorRow(1,"1","1",4444,"1",1,"1","1") val b = CrmContractorRow(22,"22","22",22,"55555",22,"nine long","22") val c = CrmContractorRow(333,"333","333",333,"333",333,"333","333") val rows = List(a,b,c) val result = rows // get for each element a Vector of (fieldName -> stringField) pairs for the string fields .map(r => gen.to(r).foldLeft(Vector[(String, String)]())(filterStrings)) // get the maximum for each "column" .reduceLeft((best, row) => best.zip(row).map { case ( kv1@ (_, v1), (_, v2)) if v1.length > v2.length => kv1 case (_, kv2) => kv2 }) result foreach { case (k, v) => println(s"$k: $v") } 
+3
source share

You probably want to use Scala reflection:

 import scala.reflect.runtime.universe._ val rm = runtimeMirror(getClass.getClassLoader) val instanceMirrors = rows map rm.reflect typeOf[CrmContractorRow].members collect {  case m: MethodSymbol if m.isCaseAccessor && m.returnType =:= typeOf[String] =>    val maxValue = instanceMirrors map (_.reflectField(m).get.asInstanceOf[String]) maxBy (_.length)    println(s"${m.name}: $maxValue") } 

To avoid problems with cases such as:

 case class CrmContractorRow(id: Long, bankCharges: String, overTime: String, name$id: Long, mgmtFee: String, contractDetails$id: Long, email: String, copyOfVisa: String) { val unwantedVal = "jdjd" } 

Greetings

+2
source share

I reworked your code for something more than once:

 import scala.reflect.ClassTag case class CrmContractorRow( id: Long, bankCharges: String, overTime: String, name$id: Long, mgmtFee: String, contractDetails$id: Long, email: String, copyOfVisa: String) object Go{ def main(args: Array[String]) { val a = CrmContractorRow(1,"1","1",4444,"1",1,"1","1") val b = CrmContractorRow(22,"22","22",22,"55555",22,"nine long","22") val c = CrmContractorRow(333,"333","333",333,"333",333,"333","333") val rows = List(a,b,c) val initEmptyColumns = List.fill(a.productArity)(List()) def aggregateColumns[Tin:ClassTag,Tagg](rows: Iterable[Product], aggregate: Iterable[Tin] => Tagg) = { val columnsWithMatchingType = (0 until rows.head.productArity).filter { index => rows.head.productElement(index) match {case t: Tin => true; case _ => false} } def columnIterable(col: Int) = rows.map(_.productElement(col)).asInstanceOf[Iterable[Tin]] columnsWithMatchingType.map(index => (index,aggregate(columnIterable(index)))) } def extractCaseClassFieldNames[T: scala.reflect.ClassTag] = { scala.reflect.classTag[T].runtimeClass.getDeclaredFields.filter(!_.isSynthetic).map(_.getName) } val agg = aggregateColumns[String,String] (rows,_.maxBy(_.length)) val fieldNames = extractCaseClassFieldNames[CrmContractorRow] agg.map{case (index,value) => fieldNames(index) + ": "+ value}.foreach(println) } } 

Using formless will get rid of .asInstanceOf, but the gist will be the same. The main problem with this code was that it was not reused, since the aggregation logic was mixed with the reflection logic to get the field names.

0
source share

All Articles