一般在具有相同形状的两个案例类别之间进行转换

时间:2019-04-29 21:19:30

标签: scala shapeless case-class

我有一堆案例类,在其他密封特征中具有相同形状的对应类(每个密封特征用于Akka Typed行为中的穷举模式匹配),并且我希望使用最少的样板从一个版本转换到下一个版本。

特征看起来像这样:

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
}

我知道我可以像这样使用shapeless.Generic进行转换:

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

但是每次转换都要做的不只是样板 手动构建案例类。理想情况下,我希望使用一个基于编译时提供的两种类型(例如val msg2 = convert(msg1)

)派生上述代码的转换器。

为此,我尝试将其分解为以下内容:

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

但这会导致:

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

通过挖掘,似乎我需要使用Generic.Aux来引导我:

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

其中,当通过以下方式调用时:

val msg3 = convert(msg2)

导致:

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

这是可以理解的,因为没有定义返回类型。但是,我弄清楚了如何提示B是什么,以便可以隐式派生genB

2 个答案:

答案 0 :(得分:3)

您可以使用“部分应用程序”

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]

(最好使用@Ben的答案中的“部分应用程序”)

或创建类型类

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案例研究:案例类迁移”

答案 1 :(得分:2)

正如您所建议的,问题在于未指定结果类型并且无法推断结果类型。您可以通过显式提供类型参数来解决此问题,如

val msg3 = convert[ProtocolCommands.ProtocolMsg, RoutingCommands.ProtocolMsg, String :: Int :: HNil](msg2)

但这显然使使用Shapeless失去了意义。编译器只需要显式指定返回类型,并且可以推断其他类型,但是Scala不直接支持仅显式提供类型参数的子集。

如上一个答案中所述,您可以使用“部分应用”模式使用部分应用程序来解决此限制。如果您使用需要在返回类型上指定的类参数,而不是输入类型,则效果最好:

def convert[B] = new ConvertPartiallyApplied[B]

class ConvertPartiallyApplied[B] {
  def apply[A, Repr](a: A)(implicit genA: Generic.Aux[A, Repr], genB: Generic.Aux[B, Repr]) = genB.from(genA.to(a))
}

然后可以简单地与

一起使用
convert[RoutingCommands.ProtocolMsg](msg2)