Search code examples
scalashapelesscase-class

Generically converting between two case classes of the same shape


I have a bunch of case classes that have identically shaped counterparts in other sealed traits (each sealed trait is used for exhaustive pattern matching in Akka Typed behaviors) and I want to convert from one version to the next with the least boilerplate.

The traits look something like this:

object RoutingCommands {
  sealed trait Command
  final case class ProtocolMsg(name: String, id: Int) extends Command
}

object ProtocolCommands {
  sealed trait Command
  final case class ProtocolMsg(name: String, id: Int) extends Command
}

I know I can do the conversion using shapeless.Generic like this:

val msg1 = ProtocolCommands.ProtocolMsg("foo", 1)
val msg2 = Generic[RoutingCommands.ProtocolMsg].from(
  Generic[ProtocolCommands.ProtocolMsg].to(msg1)
)

But having to do that for every conversion is more boilerplate than just constructing the case classes by hand. Ideally, I'd like a converter that derives the above code based on the two types provided at compile time, such as val msg2 = convert(msg1)

As a step toward that I tried to break it down to something like:

def convert[A,B](a: A): B = Generic[B].from(
  Generic[A].to(a)
)

but that results in:

Error:(55, 44) could not find implicit value for parameter gen: shapeless.Generic[B]

From digging around, it seems I need to use Generic.Aux which lead me to:

def convert[A, B, HL <: HList](a: A)(
  implicit
  genA: Generic.Aux[A, HL],
  genB: Generic.Aux[B, HL]
) = genB.from(genA.to(a))

Which, when called with:

val msg3 = convert(msg2)

results in:

Error:(61, 57) could not find implicit value for parameter genB: shapeless.Generic.Aux[B,HL]

This is understandable since nowhere is the return type defined. However, I figure out how to provide a hint what B is so that genB can be derived implicitly.


Solution

  • You can use "partial application"

    def convert[A, HL <: HList](a: A)(
      implicit
      genA: Generic.Aux[A, HL]
    ) = new Helper(a, genA)
    
    class Helper[A, HL <: HList](a: A, genA: Generic.Aux[A, HL]) {
      def apply[B](implicit genB: Generic.Aux[B, HL]) = genB.from(genA.to(a))
    }
    
    val msg3 = convert(msg2).apply[ProtocolCommands.ProtocolMsg]
    

    (it's better to use "partial application" from @Ben's answer)

    or create a type class

    trait Convert[A, B] {
      def apply(a: A): B
    }
    
    object Convert {
      implicit def mkConvert[A, B, HL <: HList](implicit
        genA: Generic.Aux[A, HL],
        genB: Generic.Aux[B, HL]
      ): Convert[A, B] = a => genB.from(genA.to(a))
    }
    
    implicit class ConvertOps[A](a: A) {
      def convert[B](implicit cnv: Convert[A, B]): B = cnv(a)
    }
    
    val msg3 = msg2.convert[ProtocolCommands.ProtocolMsg]
    

    https://books.underscore.io/shapeless-guide/shapeless-guide.html#sec:ops:migration "6.3 Case study: case class migrations"