我可以在Scala3中使用镜像来合成ADT的匹配语句吗?

eivnm1vs  于 2022-11-09  发布在  Scala
关注(0)|答案(2)|浏览(159)

我想使用Mirror或其他一些技术来序列化ADT。
我的具体用例是通过通道序列化消息。我可以用Case类对消息进行建模;这很容易。这给了我以下代码:

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

在我看来,我有两个问题:

  • 如何在类型级别保证所有Messsage Case类只包含具有serialize方法的字段?
  • 如何将m: Message转换为可对其执行操作的泛型“可序列化”元组?

我可以用match。那么核心逻辑就是:

def serializeMessage(m: Message) = m match {
  case a: A => Tuple.fromProductTyped(a).toList.map(_.serialize)
  case b: B => Tuple.fromProductTyped(b).toList.map(_.serialize)
}

这将进行编译。不幸的是,我的API有50多条消息,而且我可能还想支持序列化以外的用例,所以我想自动派生。它完全是机械的,而且非常重复,所以我觉得它“应该”是可行的。

sigwle7e

sigwle7e1#

您可以使用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
或者,您可以引入一个类型类和derive(例如,使用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

实际上,在引擎盖下,无形3使用的是scala.deriving.Mirror
How to access parameter list of case class in a dotty macro
Using K0.ProductInstances in shapeless3

oyxsuwqo

oyxsuwqo2#

根据Dmytro Mitin的回答(谢谢!),我想出了一个使用Scala Mirror的解决方案,它不需要无形状或宏。
这在很大程度上基于标准库中的类型类派生示例: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

其主旨是:

  • 创建一个我们可以从中派生的类型类Serializable[T]
  • T类型的任意元组结构实现类型类示例
  • 切入点在inline given derived[T]: Serializable[T]。在这个方法中,我们是using一个镜像,Scala已经为所有只将case类作为子类型的密封特征提供了镜像(也可以参见上面链接的文档)。Mirror包含我们需要的所有信息,以获得我们想要序列化的消息的通用元组结构。此消息的类型为T
  • 我们使用match来确定我们的消息是产品类型还是总和类型。这面镜子保存着必要的信息来弄清楚这一点。我们调用各自的序列化程序。对于elems参数,我们为所有镜像的元素类型调用(summonAll)序列化程序类型类示例。例如,如果我们想要序列化case class C(i: Int, s: String),那么这就是产品案例,所以我们使用p,它是产品镜像。p.MirroredElemTypes(Int, String)Tuple.Map[p.MirroredElemTypes, Serializable]](Serializable[Int], Serializable[String])
  • productSerializer方法(这是两个方法中比较复杂的一个)将这些序列化程序作为elems参数接收。产品镜像p告诉我们要序列化的消息类型(m: T)的结构。elems是实际的序列化程序。因此,我们将melems转换为可迭代代码,将它们压缩并将m的每个部分Map到其序列化版本。然后我们通过mkString连接序列化的部分。有趣的是,在这一点上,镜子甚至没有被使用。教程也是这样做的,所以我不确定镜子是用来做什么的,但如果我们使用得当,也许我们可以去掉一些instanceOf。我的实现可能并不理想。
  • 为常见的基本类型提供given类型类示例。我以StringInt为例。我提供了一个Seq的示例,它也可以用于任何扩展Seq的集合。

相关问题