I'd like to use Mirror
s or some other technology to serialize ADTs.
My concrete use case is that I'm serializing messages over a channel. I can model the messages with case classes; that's easy enough. That gives me the following code:
sealed trait Message
final case class A(a: Int) extends Message
final case class B(b: Int, s: String) extends Message
def serializeMessage(m: Message) =
Tuple.fromProductTyped(m).toList.map(_.serialize) // doesn't work because `m` is a Sum
type Primitive = Int | String
extension (p: Primitive)
def serialize = p match {
case i: Int => s"an Int: $i"
case s: String => s"an String: $s"
}
As far as I can see, I have two problems:
- How can I guarantee at type level that all messsage case classes only include fields that have a
serialize
method available? - How do I convert an
m: Message
to a generic tuple of "serializables" that I can act on?
I could use match
. The core logic is then:
def serializeMessage(m: Message) = m match {
case a: A => Tuple.fromProductTyped(a).toList.map(_.serialize)
case b: B => Tuple.fromProductTyped(b).toList.map(_.serialize)
}
This compiles. Unfortunately, my API has 50 messages, and I might also want to support usecases other than serialization, so I'd like to automate the derivation. It's perfectly mechanical and very repetitive, so I feel like it "should" be doable.
CodePudding user response:
You can automate the pattern matching with a macro
import scala.quoted.{Expr, Quotes, quotes, Type}
import scala.deriving.Mirror
inline def serializeMessage(m: Message): List[String] = ${serializeMessageImpl('m)}
def serializeMessageImpl(m: Expr[Message])(using Quotes): Expr[List[String]] = {
import quotes.reflect.*
val caseDefs = TypeRepr.of[Message].typeSymbol.children.map(symb => {
val typeTree = TypeTree.ref(symb)
val typeRepr = typeTree.tpe
val bind = Symbol.newBind(Symbol.spliceOwner, "x", Flags.EmptyFlags, typeRepr)
val ref = Ref(bind)
typeRepr.asType match {
case '[a0] =>
'{tag[a0]} match {
case '{
type a <: Product
tag[`a`]
} => {
val mirror = Expr.summon[Mirror.ProductOf[a]].getOrElse(
report.errorAndAbort(s"Can't find Mirror.ProductOf[${Type.show[a]}]")
)
CaseDef(
Bind(bind, Typed(ref, typeTree)),
None,
'{Tuple.fromProductTyped(${ref.asExprOf[a]})(using $mirror).toList.asInstanceOf[List[Primitive]].map(_.serialize)}.asTerm
)
}
}
}
})
Match(m.asTerm, caseDefs).asExprOf[List[String]]
}
def tag[A] = ???
serializeMessage(B(1, "abc")) // List(an Int: 1, an String: abc)
//scalac: m$proxy1 match {
// case x @ x => // case x: A =>
// scala.Tuple.fromProductTyped[Macro.A](x)(Macro.A.$asInstanceOf$[scala.deriving.Mirror.Product {
// type MirroredMonoType >: Macro.A <: Macro.A
// type MirroredType >: Macro.A <: Macro.A
// type MirroredLabel >: "A" <: "A"
// type MirroredElemTypes >: scala.*:[scala.Int, scala.Tuple$package.EmptyTuple] <: scala.*:[scala.Int, scala.Tuple$package.EmptyTuple]
// type MirroredElemLabels >: scala.*:["a", scala.Tuple$package.EmptyTuple] <: scala.*:["a", scala.Tuple$package.EmptyTuple]
// }]).toList.asInstanceOf[scala.List[Macro.Primitive]].map[java.lang.String](((_$1: Macro.Primitive) => Macro.serialize(_$1)))
// case x @ `x₂` => // case x: B =>
// scala.Tuple.fromProductTyped[Macro.B](`x₂`)(Macro.B.$asInstanceOf$[scala.deriving.Mirror.Product {
// type MirroredMonoType >: Macro.B <: Macro.B
// type MirroredType >: Macro.B <: Macro.B
// type MirroredLabel >: "B" <: "B"
// type MirroredElemTypes >: scala.*:[scala.Int, scala.*:[scala.Predef.String, scala.Tuple$package.EmptyTuple]] <: scala.*:[scala.Int, scala.*:[scala.Predef.String, scala.Tuple$package.EmptyTuple]]
// type MirroredElemLabels >: scala.*:["b", scala.*:["s", scala.Tuple$package.EmptyTuple]] <: scala.*:["b", scala.*:["s", scala.Tuple$package.EmptyTuple]]
// }]).toList.asInstanceOf[scala.List[Macro.Primitive]].map[java.lang.String](((`_$1₂`: Macro.Primitive) => Macro.serialize(`_$1₂`)))
//}
Scala 3 collection partitioning with subtypes
https://github.com/lampepfl/dotty/discussions/12472
Alternatively you can introduce a type class and derive it (e.g. with Shapeless 3)
libraryDependencies = Seq(
"org.typelevel" %% "shapeless3-deriving" % "3.2.0",
"org.typelevel" %% "shapeless3-typeable" % "3.2.0"
)
import shapeless3.deriving.K0
import shapeless3.typeable.Typeable
trait Serializer[T]:
def serialize(t: T): String
trait LowPrioritySerializer:
given [T](using typeable: Typeable[T]): Serializer[T] with
override def serialize(t: T): String = s"an ${typeable.describe}: $t"
object Serializer extends LowPrioritySerializer:
given prod[T](using inst: K0.ProductInstances[Serializer, T]): Serializer[T] with
override def serialize(t: T): String = inst.foldRight[String](t)("")(
[a] => (s: Serializer[a], x: a, acc: String) =>
s.serialize(x) (if acc.isEmpty then "" else ", ") acc
)
given coprod[T](using inst: K0.CoproductInstances[Serializer, T]): Serializer[T] with
override def serialize(t: T): String = inst.fold[String](t)(
[a <: T] => (s: Serializer[a], x: a) => s.serialize(x)
)
extension [T: Serializer](t: T)
def serialize = summon[Serializer[T]].serialize(t)
A(1).serialize // an Int: 1
B(1, "abc").serialize // an Int: 1, an String: abc
(A(1): Message).serialize // an Int: 1
(B(1, "abc"): Message).serialize // an Int: 1, an String: abc
Actually, under the hood Shapeless 3 uses scala.deriving.Mirror
.