6

Is there a way to convert one case class to another when they have the same fields and inherit from the same trait, without providing a converter function (that would simply do the one to one field mapping)?

For example:

trait UberSomething {
  val name: String
}
// these may be located in different files
case class Something(name: String) extends UberSomething
case class SomethingOther(name: String) extends UberSomething

val s = Something("wtv")
//s.asInstanceOf[SomethingOther] FAILS
scc
  • 10,342
  • 10
  • 51
  • 65
  • 1
    Shapeless can definitely do this, have a look at http://stackoverflow.com/questions/23192760/safely-copying-fields-between-case-classes-of-different-types – Angelo Genovese Apr 12 '17 at 13:55

2 Answers2

4

First of all never define trait members as val if they are meant to be implemented at a later point.

trait UberSomething {
  def name: String
}
// these maybe in different files
case class Something(name: String) extends UberSomething
case class SomethingOther(name: String) extends UberSomething

import shapeless._, ops.hlist.Align

Another approach I've seen somewhere on Stackoverflow before, so apologies for stealing street cred, is to use Align such that order of the fields wouldn't matter.

class Convert[Target] {
  def apply[Source, HLS <: HList, HLT <: HList](s: Source)(implicit

    // Convert the Source to an HList type
    // include field names, e.g "labelled"
    // Shapeless "generates" this using an implicit macro
    // it looks at our type, extracts a list of (Name, Type) pairs
    genS: LabelledGeneric.Aux[Source, HLS],

    // Convert the Target o an HList type
    // include field names, e.g "labelled"
    // So again we have a (Name, Type) list of pairs this time for Target
    genT: LabelledGeneric.Aux[Target, HLT],

    // Use an implicit align to make sure the two HLists
    // contain the same set of (Name, Type) pairs in arbitrary order.
    align: Align[HLS, HLT]
  ) = genT from align(genS to s)
}
// Small trick to guarantee conversion only requires
// a single type argument, otherwise we'd have to put something
// in place for HLS and HLT, which are meant to be path dependant
// and "calculated" by the LabelledGeneric.Repr macro so it wouldn't work as it breaches the "Aux pattern", which exposes a type member materialized by a macro in this case.
// HLT and HLS come from within genS.Repr and genT.Repr.
def convert[T] = new Convert[T]

This is a bit better as the HList params are nicely masked as part of apply so you don't trip yourself up.

val sample = Something("bla")
convert[SomethingOther](sample) // SomethingOther("bla")

Let's review this line: genT from align(genS to s).

  • First genS to s converts the Source instance to a LabelledGeneric, e.g an HList with field info.

  • Align aligns the types and fields of the created HList for the Source type to match the Target type.

  • genT from .. allows us to create an instance of Target from an HList granted the compiler can "prove" the fields and types are "all there", which is something we already have with Align.

flavian
  • 28,161
  • 11
  • 65
  • 105
  • That doesn't actually compile in `gen2 to (gen from obj)` – scc Apr 12 '17 at 14:23
  • Also, "never define trait members as val if they are meant to be implemented at a later point." - can you elaborate a bit or point me to relevant links? Thanks :) – scc Apr 12 '17 at 14:27
  • 1
    @Sofia http://stackoverflow.com/questions/19642053/when-to-use-val-or-def-in-scala-traits. I've updated my answer. – flavian Apr 12 '17 at 14:31
  • @AssafMendelson I've added explanations, I don't have the time to detail more, but I recommend understanding `Generic` and `LabelledGeneric` in shapeless as once you do this will be a pretty simple case of using those typeclasses. – flavian Apr 12 '17 at 14:41
3

You can do that using implicit conversions, eg:

trait UberSomething {
  val name: String
}

case class Something(name: String) extends UberSomething
case class SomethingOther(name: String) extends UberSomething

object Something {
  implicit def somethingToSomethingOther(s:Something):SomethingOther = SomethingOther(s.name)
}
object SomethingOther {
  implicit def somethingOtherToSomething(s:SomethingOther):Something = Something(s.name)
}

val s = Something("wtv")
val so:SomethingOther = s
Kris
  • 5,714
  • 2
  • 27
  • 47
  • Yes, I know. Creating those conversion functions (implicit or not) is what I'm trying to avoid.. – scc Apr 12 '17 at 14:06