在相同形状的两个 case 类 之间进行一般转换
Generically converting between two case classes of the same shape
我有一堆案例 类 在其他密封特征中具有相同形状的对应物(每个密封特征用于 Akka 类型行为中的详尽模式匹配),我想从一个版本转换为接下来是最少的样板文件。
特征看起来像这样:
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]
这是可以理解的,因为没有地方定义 return 类型。但是,我想出了如何提供 B
是什么的提示,以便可以隐式地导出 genB
。
您可以使用 "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]
(最好使用@Ben 的回答中的"partial application")
或创建类型 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 案例研究:案例 class 迁移”
正如您所建议的,问题是结果类型未指定且无法推断。您可以通过显式提供类型参数来解决这个问题,如
val msg3 = convert[ProtocolCommands.ProtocolMsg, RoutingCommands.ProtocolMsg, String :: Int :: HNil](msg2)
但这显然违背了使用 Shapeless 的目的。编译器只需要显式指定 return 类型,就可以推断出其他类型,但 Scala 不直接支持仅显式提供类型参数的子集。
如前一个答案中所述,您可以使用 "partially applied" 模式使用部分应用程序来解决此限制。如果您使用在 return 类型上参数化的 class ,这是您需要指定的,而不是输入类型:
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)
我有一堆案例 类 在其他密封特征中具有相同形状的对应物(每个密封特征用于 Akka 类型行为中的详尽模式匹配),我想从一个版本转换为接下来是最少的样板文件。
特征看起来像这样:
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]
这是可以理解的,因为没有地方定义 return 类型。但是,我想出了如何提供 B
是什么的提示,以便可以隐式地导出 genB
。
您可以使用 "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]
(最好使用@Ben 的回答中的"partial application")
或创建类型 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 案例研究:案例 class 迁移”
正如您所建议的,问题是结果类型未指定且无法推断。您可以通过显式提供类型参数来解决这个问题,如
val msg3 = convert[ProtocolCommands.ProtocolMsg, RoutingCommands.ProtocolMsg, String :: Int :: HNil](msg2)
但这显然违背了使用 Shapeless 的目的。编译器只需要显式指定 return 类型,就可以推断出其他类型,但 Scala 不直接支持仅显式提供类型参数的子集。
如前一个答案中所述,您可以使用 "partially applied" 模式使用部分应用程序来解决此限制。如果您使用在 return 类型上参数化的 class ,这是您需要指定的,而不是输入类型:
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)