1

I'd like to use Mirrors 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.

PawkyPenguin
  • 219
  • 2
  • 9

2 Answers2

1

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.

How to access parameter list of case class in a dotty macro

Using K0.ProductInstances in shapeless3

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
1

Based on Dmytro Mitin's answer (thank you!), I came up with a solution using Scala Mirrors, and which does not require shapeless or macros.

This is largely based on the type class derivation example in the standard library: https://docs.scala-lang.org/scala3/reference/contextual/derivation.html#mirror

// a bunch of messages for testing
sealed trait Message derives Serializable
final case class A(p: Int) extends Message
final case class E() extends Message
sealed trait Submessage extends Message
final case class Sub(a: Vector[String], i: Int) extends Submessage


trait Serializable[T] {
  def serialize(m: T): String
}

object Serializable {
  import deriving.Mirror

  inline given derived[T](using m: Mirror.Of[T]): Serializable[T] = 
    inline m match {
      case p: Mirror.ProductOf[_] =>
        productSerializer(p, compiletime.summonAll[Tuple.Map[p.MirroredElemTypes, Serializable]])
      case s: Mirror.SumOf[_] =>
        sumSerializer(s, compiletime.summonAll[Tuple.Map[s.MirroredElemTypes, Serializable]])
    }
  

  def productSerializer[T](p: Mirror.ProductOf[T], elems: Tuple.Map[p.MirroredElemTypes, Serializable]) =
    new Serializable[T] {
      def serialize(m: T) = {
        val messageIterator = m.asInstanceOf[Product].productIterator
        val serializableForT = elems.toList.asInstanceOf[List[Serializable[Any]]].iterator
        val serializedParts = messageIterator.zip(serializableForT).map {
          case (mPart, mSerializer) => mSerializer.serialize(mPart)
        }
        serializedParts.mkString(", ")
      }
    }

  def sumSerializer[T](s: Mirror.SumOf[T], elems: Tuple.Map[s.MirroredElemTypes, Serializable]) =
    new Serializable[T] {
      def serialize(m: T): String = {
        val caseOfM = s.ordinal(m)
        elems.toList.asInstanceOf[List[Serializable[T]]](caseOfM).serialize(m)
      }
    }


  
  /* Provide `serialize` as an extension method, and implement some common typeclass instances */
  extension[T](m: T) {
    def serialize(using s: Serializable[T]): String = s.serialize(m)
  }
  
  given Serializable[Int] with {
    def serialize(m: Int) = s"an Int: $m"
  }
  
  given Serializable[String] with {
    def serialize(m: String) = s"a String: `$m`"
  }

  given seqInstance[T, S[T] <: Seq[T]](using s: Serializable[T]): Serializable[S[T]] with {
    def serialize(a: S[T]) = a.map(s.serialize(_)).mkString(", ")
  }
}

import Playground.Serializable.serialize
println(A(5).serialize) //an Int: 5
println(E().serialize)  //[empty String]
println(Sub(Vector("a", "b"), 1000).serialize) //a String: `a`, a String: `b`, an Int: 1000

The gist of it is:

  • Create a typeclass Serializable[T] which we can derive from.
  • Implement typeclass instances for arbitrary tuple structures of type T
    • The entry point is inline given derived[T]: Serializable[T]. In this method, we are using a Mirror that Scala already provides for all sealed traits that only have case classes as subtypes (also enums, see documentation linked above). The Mirror holds all information we need, to get the generic tuple-structure of a message that we want to serialize. This message is of type T.
    • We use a match to decide whether our message is a product or a sum type. The mirror holds the necessary information to figure that out. We call the respective serializer. For the elems argument, we summon (summonAll) serializer typeclass instances for all mirrored element types. For example, if we want to serialize a case class C(i: Int, s: String), then this is the product case, so we use p, which is the product mirror. p.MirroredElemTypes is (Int, String), and Tuple.Map[p.MirroredElemTypes, Serializable]] is (Serializable[Int], Serializable[String]).
    • The productSerializer method (which is the more complicated of the two) receives those serializers as the elems argument. The product mirror p tells us the structure of the message type (m: T) we want to serialize. The elems are the actual serializers. So we convert both m and elems to iterables, zip them and map each part of m to its serialized version. Then we join the serialzed parts by mkString. Interestingly, the mirror isn't even used at this point. The tutorial does it this way too, so I'm not sure what the mirror is for, but maybe we could get rid of some instanceOfs if we used it properly. My implementation is probably not ideal.
  • Provide given type class instances for common base types. I used String and Int as an example. I included an example for Seq that can also be used for any collection extending Seq.
PawkyPenguin
  • 219
  • 2
  • 9