analystic
analystic

Reputation: 351

Case classes - Copy multiple fields with transforms

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.

Upvotes: 2

Views: 872

Answers (3)

jwvh
jwvh

Reputation: 51271

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)

Upvotes: 1

Tim
Tim

Reputation: 27356

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.

Upvotes: 2

J0HN
J0HN

Reputation: 26921

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

Upvotes: 2

Related Questions