Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala case class conversion

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
like image 720
scc Avatar asked Apr 12 '17 13:04

scc


2 Answers

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.

like image 78
flavian Avatar answered Oct 30 '22 21:10

flavian


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
like image 42
Kris Avatar answered Oct 30 '22 21:10

Kris