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.
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"