2

I have a case class with 25 fields and need to convert it into another with 22, of which 19 of these are shared and 3 are simply renamed.

I have found a few examples of how to do this using shapeless (e.g. an answer here and some code examples from Miles Sabin here and here) but the last of those looks a bit out of date, and I can't figure out from the Github example how I can use shapeless to rename multiple fields, or do more manipulation on a field before adding it to the new object. Can anyone help me out?

Simplified code example;

import shapeless.LabelledGeneric
case class A(fieldA:Int, fieldB:String, fieldC:String)
case class B(fieldARenamed:Int, fieldB:String, fieldC:String, fieldCTransformed:String)

val aGen = LabelledGeneric[A]
val bGen = LabelledGeneric[B]

val freddie = new A(1,"Freddie","somestring")

val record = aGen.to(freddie)
val atmp = freddie.fieldA
record.Remove("fielda")

val freddieB = bGen.from(record + 
  (Symbol("fieldARenamed") ->> atmp) +
  (Symbol("fieldCTransformed") ->> freddie.fieldC.toUpperCase)
) //Errors everywhere, even if I replace + with :: etc.

I have a feeling Align is going to come into the picture somewhere here, but understanding how to do this in the leanest possible fashion - e.g. without creating additional traits like Field, as in that third link above - would be interesting.

In The Shapeless Guide, there is also some usage of a single quote, (e.g. 'fieldC) notation, which I haven't been able to find much information on, so if that plays a role some explanation would also be really helpful. Fairly new to this depth of Scala sorcery, so apologies if the question seems obtuse or covers too many disparate topics.

EDIT: For the avoidance of doubt, I am not looking for answers which suggest that I just manually create a new case class by referencing fields from the first, as in;

val freddieB = B(fieldARenamed = freddie.fieldA, fieldB = freddie.fieldB, fieldC = freddie.fieldC, fieldCTransformed =freddie.fieldC.toUpperCase)

See below comment for various reasons why this is inappropriate.

analystic
  • 351
  • 5
  • 17
  • Can you share your classes? – Krzysztof Atłasik May 27 '19 at 08:58
  • Simplified code example already above. – analystic May 28 '19 at 05:40
  • The shapeless [guide](https://books.underscore.io/shapeless-guide/shapeless-guide.html) has a case study (6.3) that does most of what you want. What it won't do is transfer/transform an existing value to a new or renamed field. That kind of thing _can_ be done but I don't know if it can be generalized. The field names and types have to be known at compile-time so you end up writing a lot of code specific for `A`-to-`B` transitions and only for `A`-to-`B` transitions. – jwvh May 30 '19 at 08:13

3 Answers3

2

The simplest solution is to construct an instance of the new case class using the values from the old one, applying functions to the values as necessary. The code will be very efficient, the purpose of the code will be very clear, it will take less time to write than any other solution, it will be more robust and maintainable than a solution that depends on third-party libraries, and it avoids a hidden dependency between the two classes.

Tim
  • 26,753
  • 2
  • 16
  • 29
  • That's my current solution. Not very elegant however, considering it results in 25 fields being typed out. Moreover, these case classes are automatically generated from database schemas. I'd like to be able to change these without having to touch the code if the database changes (assuming that the changes to the database don't touch fields that are common between both case classes and therefore aren't manipulated by the code). – analystic May 28 '19 at 05:41
  • Also, downvoting, as the question was pretty clear that I'd like an answer that uses `shapeless` (or at least something similar) to copy the shared fields without explicitly typing every one out. – analystic May 28 '19 at 05:47
  • Elegance disappeared when you created a `case class` with 25 fields; fancy libraries and obscure code are not going to bring the elegance back. – Tim May 28 '19 at 05:48
  • Some code interacts with databases in the real world :) – analystic May 28 '19 at 05:50
  • Database records with 25 fields aren't elegant either! And talking of the real world, any change to the database is going to need significant review and a re-write of a number of tests, so editing a few lines in this code is really not a big deal. – Tim May 28 '19 at 05:52
2

One other option is to use automapper; in particular, Dynamic Mappings feature.

For your particular example it would look like the following:

import io.bfil.automapper._

case class A(fieldA:Int, fieldB:String, fieldC:String)
case class B(fieldARenamed:Int, fieldB:String, fieldC:String, fieldCTransformed:String)

val freddie = new A(1,"Freddie","somestring")

val freddieB = automap(freddie).dynamicallyTo[B](
  fieldARenamed = freddie.fieldA, 
  fieldCTransformed = freddie.fieldC.toUpperCase
)

and I guess you can make it a function

def atob(a: A): B = {
  automap(a).dynamicallyTo[B](
    fieldARenamed = a.fieldA, 
    fieldCTransformed = a.fieldC.toUpperCase
  )
}

From efficiency point of view, this lib uses macros, so the generated code is practically as good as one could've written by hand

J0HN
  • 26,063
  • 5
  • 54
  • 85
1

Just FYI, here's one way to get your question code to work.

import shapeless._
import shapeless.labelled.FieldType
import shapeless.ops.hlist.{Align,Intersection}
import shapeless.syntax.singleton._

case class A(fieldA:Int, fieldB:String, fieldC:String)
case class B(fieldARenamed:Int, fieldB:String, fieldC:String, fieldCTransformed:String)

val fromGen = LabelledGeneric[A]
val toGen   = LabelledGeneric[B]

val freddie = A(1, "Freddie", "somestring")
val putARename = Symbol("fieldARenamed")     ->> freddie.fieldA
val putCTrans  = Symbol("fieldCTransformed") ->> freddie.fieldC.toUpperCase

trait Field { type K; type V; type F = FieldType[K, V] }
object Field {
  def apply[K0,V0](sample: FieldType[K0,V0]) =
    new Field { type K = K0; type V = V0 }
}

val pFieldA  = Field(putARename)
val pFieldCT = Field(putCTrans)

val inter = Intersection[pFieldA.F :: pFieldCT.F :: fromGen.Repr, toGen.Repr]
val align = Align[inter.Out, toGen.Repr]

toGen.from(align(inter(putARename :: putCTrans :: fromGen.to(freddie))))
//res0: B = B(1,Freddie,somestring,SOMESTRING)
jwvh
  • 50,871
  • 7
  • 38
  • 64
  • Still requires more boilerplate than I'd hoped for but marking as the answer as it is as close as we'll get I think. – analystic Jun 04 '19 at 01:03