Scala3:通过元编程创建类型?

Jea*_*ler 3 scala metaprogramming scala-macros scala-3

我正在使用 scala3 进行编码,利用编程结构类型。结构类型恰好模仿了现有的案例类:它们的定义是纯粹的样板,因此很容易通过元编程来制作它们。

我了解如何制作函数实现,通常是通过类型类派生。但在这里我们正在尝试制作一个(结构)类型

这在 scala2 中是可能的,通过类宏注释,但这些在 scala3 中已经消失了。有办法吗?如果是这样怎么办?

下面的代码是我想获得的结果:


// Library part
trait View extends scala.Selectable :
  def selectDynamic(key:String) =
    println(s"$key is being looked up")
    ???


// DSL Definition part
case class SomeDefWithInt   ( i : Int    )
case class SomeDefWithString( s : String )

// Boiler-plate code 
type ViewOf[M] = M match
  case SomeDefWithInt    => View { def i : Int    }
  case SomeDefWithString => View { def s : String }

// Mockup usage
class V extends View
val v = V()

v.asInstanceOf[ViewOf[SomeDefWithInt   ]].i
v.asInstanceOf[ViewOf[SomeDefWithString]].s
Run Code Online (Sandbox Code Playgroud)

是否可以创建任意案例类 M 的 ViewOf[M] ?

谢谢 !

Dmy*_*tin 5

以防万一,这就是我所说的隐藏ViewOf在类型类中的意思(类型类是匹配类型的替代方案)。遗憾的是,在 Scala 3 中这是冗长的。

(版本1)

import scala.annotation.experimental
import scala.quoted.{Expr, Quotes, Type, quotes}

// Library part
trait View extends Selectable {
  def applyDynamic(key: String)(args: Any*): Any = {
    println(s"$key is being looked up with $args")
    if (key == "i") 1
    else if (key == "s") "a"
    else ???
  }

  def selectDynamic(key: String): Any = {
    println(s"$key is being looked up")
    if (key == "i1") 2
    else if (key == "s1") "b"
    else ???
  }
}

// type class
trait ViewOf[M <: Product] {
  type Out <: View
}

object ViewOf {
  transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}

  @experimental // because .newClass is @experimental
  def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
    import quotes.reflect.*

    extension (symb: Symbol) {
      def setFlags(flags: Flags): Symbol = {
        given dotty.tools.dotc.core.Contexts.Context =
          quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
        symb.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
          .denot.setFlag(flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet])
        symb
      }
    }

    def newType(cls: Symbol, name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags, privateWithin: Symbol = Symbol.noSymbol): Symbol = {
      given dotty.tools.dotc.core.Contexts.Context =
        quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
      import dotty.tools.dotc.core.Decorators.toTypeName
      dotty.tools.dotc.core.Symbols.newSymbol(
        cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
        name.toTypeName,
        flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
        tpe.asInstanceOf[dotty.tools.dotc.core.Types.Type],
        privateWithin.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
      ).asInstanceOf[Symbol]
    }

    val M = TypeRepr.of[M]
    val fields = M.typeSymbol.caseFields

    val viewImplDecls = (cls: Symbol) =>
      fields.flatMap(fieldSymb =>
        Seq(
          Symbol.newMethod(cls, fieldSymb.name, MethodType(Nil)(_ => Nil, _ => M.memberType(fieldSymb)), // vararg? MatchError: Inlined
            Flags.Deferred, privateWithin = Symbol.noSymbol),
          Symbol.newVal(cls, fieldSymb.name + "1", M.memberType(fieldSymb),
            Flags.Deferred, privateWithin = Symbol.noSymbol)
        )
      )

    val viewImplParents = List(TypeTree.of[AnyRef], TypeTree.of[View])

    val viewImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewImpl", viewImplParents.map(_.tpe), viewImplDecls, selfType = None)
      .setFlags(Flags.Trait)
    val methodDefs = fields.flatMap(fieldSymb => {
      val methodSymb = viewImplCls.declaredMethod(fieldSymb.name).head
      val valSymb = viewImplCls.fieldMember(fieldSymb.name + "1")
      Seq(
        DefDef(methodSymb, _ => None),
        ValDef(valSymb, None)
      )
    })
    val viewImplClsDef = ClassDef(viewImplCls, viewImplParents, body = methodDefs)

    val viewOfImplDecls = (cls: Symbol) => List(newType(cls, "Out",
      TypeBounds(viewImplCls.typeRef, viewImplCls.typeRef), Flags.Override))

    val viewOfTypeTree = TypeTree.of[ViewOf[M]]
    val viewOfImplParents = List(TypeTree.of[AnyRef], viewOfTypeTree)

    val viewOfImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewOfImpl", viewOfImplParents.map(_.tpe), viewOfImplDecls, selfType = None)
    val outSymb = viewOfImplCls.declaredType("Out").head

    val outTypeDef = TypeDef(outSymb)
    val viewOfImplClsDef = ClassDef(viewOfImplCls, viewOfImplParents, body = List(outTypeDef))
    val newViewOfImpl = Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil)

    val res = Block(List(viewImplClsDef, viewOfImplClsDef), newViewOfImpl).asExprOf[ViewOf[M]]
    println(res.show + "=" + res.asTerm.show(using Printer.TreeStructure))
    res
  }
}

extension (v: View) {
  def refine[M <: Product](using viewOf: ViewOf[M]): viewOf.Out = v.asInstanceOf[viewOf.Out]
}
Run Code Online (Sandbox Code Playgroud)
// DSL Definition part
case class SomeDefWithInt   ( i : Int    )
case class SomeDefWithString( s : String )

// Mockup usage
class V extends View
val v = V()

println(v.refine[SomeDefWithInt].i())
// i is being looked up with ArraySeq()
// 1
println(v.refine[SomeDefWithString].s())
// s is being looked up with ArraySeq()
// a
println(v.refine[SomeDefWithInt].i1)
// i1 is being looked up
// 2
println(v.refine[SomeDefWithString].s1)
// s1 is being looked up
// b

//scalac: {
//  trait ViewImpl extends java.lang.Object with Macros.View {
//    def i(): scala.Int
//    val i1: scala.Int
//  }
//  class ViewOfImpl extends java.lang.Object with Macros.ViewOf[App.SomeDefWithInt] {
//    type Out // actually, type Out = ViewImpl
//  }
//  new ViewOfImpl()
//}=Block(List(ClassDef("ViewImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(DefDef("i", List(TermParamClause(Nil)), Inferred(), None), ValDef("i1", Inferred(), None))), ClassDef("ViewOfImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(TypeDef("Out", TypeBoundsTree(Inferred(), Inferred()))))), Apply(Select(New(Inferred()), "<init>"), Nil))

//scalac: {
//  trait ViewImpl extends java.lang.Object with Macros.View {
//    def s(): scala.Predef.String
//    val s1: scala.Predef.String
//  }
//  class ViewOfImpl extends java.lang.Object with Macros.ViewOf[App.SomeDefWithString] {
//    type Out // actually, type Out = ViewImpl
//  }
//  new ViewOfImpl()
//}=Block(List(ClassDef("ViewImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(DefDef("s", List(TermParamClause(Nil)), Inferred(), None), ValDef("s1", Inferred(), None))), ClassDef("ViewOfImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(TypeDef("Out", TypeBoundsTree(Inferred(), Inferred()))))), Apply(Select(New(Inferred()), "<init>"), Nil))
Run Code Online (Sandbox Code Playgroud)

ViewOf[M]意味着供 DSL 用户使用,因此无法将其隐藏在派生类型类中。

不确定我是否理解了。


使用 Scala 3 宏重写方法

Scala 3 宏中的“tq”等效项

如何用宏在Dotty中生成类?

如何在scala 3宏的引用语法中拼接多个表达式?

如何在点宏中访问案例类的参数列表

https://github.com/lampepfl/dotty/discussions/14056


类型类的另一种实现(使用细化类型而不是特征类型)

(版本2)

trait ViewOf[M <: Product] {
  type Out <: View
}

object ViewOf {
  transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}

  @experimental // because .newClass is @experimental
  def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
    import quotes.reflect.*

    def makeRefinement(parent: TypeRepr, names: List[String], infos: List[TypeRepr]): TypeRepr =
      names.zip(infos).foldLeft(parent){ case (acc, (name, tpe)) => Refinement(acc, name, tpe) }

    def newType(cls: Symbol, name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags, privateWithin: Symbol = Symbol.noSymbol): Symbol = {
      given dotty.tools.dotc.core.Contexts.Context =
        quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
      import dotty.tools.dotc.core.Decorators.toTypeName
      dotty.tools.dotc.core.Symbols.newSymbol(
        cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
        name.toTypeName,
        flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
        tpe.asInstanceOf[dotty.tools.dotc.core.Types.Type],
        privateWithin.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
      ).asInstanceOf[Symbol]
    }

    val M = TypeRepr.of[M]
    val fields = M.typeSymbol.caseFields

    val fieldNames = fields.flatMap(fieldSymb => Seq(fieldSymb.name, fieldSymb.name + "1"))
    val fieldMethodTypes = fields.flatMap(fieldSymb => Seq(
      MethodType(List("args"))(_ => List(AnnotatedType(TypeRepr.of[Any], '{new scala.annotation.internal.Repeated()}.asTerm)), _ => M.memberType(fieldSymb)),
      ByNameType(M.memberType(fieldSymb)))
    )
    val refinement = makeRefinement(TypeRepr.of[View], fieldNames, fieldMethodTypes)

    val viewOfImplDecls = (cls: Symbol) => List(newType(cls, "Out",
      TypeBounds(refinement, refinement), Flags.Override))

    val viewOfTypeTree = TypeTree.of[ViewOf[M]]
    val viewOfImplParents = List(TypeTree.of[AnyRef], viewOfTypeTree)

    val viewOfImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewOfImpl", viewOfImplParents.map(_.tpe), viewOfImplDecls, selfType = None)
    val outSymb = viewOfImplCls.declaredType("Out").head

    val outTypeDef = TypeDef(outSymb)
    val viewOfImplClsDef = ClassDef(viewOfImplCls, viewOfImplParents, body = List(outTypeDef))
    val newViewOfImpl = Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil)

    val res = Block(List(viewOfImplClsDef), newViewOfImpl).asExprOf[ViewOf[M]]
    println(res.show + "=" + res.asTerm.show(using Printer.TreeStructure))
    res
  }
}
Run Code Online (Sandbox Code Playgroud)
println(v.refine[SomeDefWithInt].i(10, "x", true))
//i is being looked up with ArraySeq((10,x,true))
//1
println(v.refine[SomeDefWithString].s(20, "y", 30L))
//s is being looked up with ArraySeq((20,y,30))
//a
println(v.refine[SomeDefWithInt].i1)
//i1 is being looked up
//2
println(v.refine[SomeDefWithString].s1)
//s1 is being looked up
//b

//scalac: {
//  class ViewOfImpl extends java.lang.Object with Macros.ViewOf[App.SomeDefWithInt] {
//    type Out // actually, type Out = View {def i(args: Any*): Int; def i1: Int}
//  }
//  new ViewOfImpl()
//}=Block(List(ClassDef("ViewOfImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(TypeDef("Out", TypeBoundsTree(Inferred(), Inferred()))))), Apply(Select(New(Inferred()), "<init>"), Nil))
Run Code Online (Sandbox Code Playgroud)

我们也可以使用Mirror反射来代替

(版本3)

trait ViewOf[M <: Product] {
  type Out <: View
}

object ViewOf {
  transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}

  def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
    import quotes.reflect.*

    def makeRefinement(parent: TypeRepr, namesAndTypes: List[(String, TypeRepr)]): TypeRepr =
      namesAndTypes.foldLeft(parent) { case (acc, (name, tpe)) => Refinement(acc, name, tpe) }

    def mkNamesAndTypes[mels: Type, mets: Type]: List[(String, TypeRepr)] =
      (Type.of[mels], Type.of[mets]) match {
        case ('[EmptyTuple], '[EmptyTuple]) => Nil
        case ('[mel *: melTail], '[met *: metTail] ) => {
          val name = Type.valueOfConstant[mel].get.toString
          val name1 = name + "1"
            //scala.MatchError: Inlined(Ident(Macros$),List(),Apply(Select(New(Select(Select(Select(Ident(scala),annotation),internal),Repeated)),<init>),List())) (of class dotty.tools.dotc.ast.Trees$Inlined)
          //val methodType = MethodType(List("args"))(_ => List(AnnotatedType(TypeRepr.of[Any], '{new scala.annotation.internal.Repeated()}.asTerm)), _ => TypeRepr.of[met])
          val methodType = MethodType(Nil)(_ => Nil, _ => TypeRepr.of[met])
          val methodType1 = ByNameType(TypeRepr.of[met])
          (name, methodType) :: (name1, methodType1) :: mkNamesAndTypes[melTail, metTail]
        }
      }

    val namesAndTypes = Expr.summon[Mirror.ProductOf[M]].get match {
      case '{ $m: Mirror.ProductOf[M] { type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
        mkNamesAndTypes[mels, mets]
    }

    val res = makeRefinement(TypeRepr.of[View], namesAndTypes).asType match {
      case '[tpe] =>
        '{
          new ViewOf[M] {
            type Out = tpe
          }
        }
    }

    println(res.show)
    res
  }
}
Run Code Online (Sandbox Code Playgroud)

Expr不幸的是,由于额外的类型归属(失去类型细化),这不起作用

//scalac: {
//  final class $anon() extends Macros.ViewOf[App.SomeDefWithInt] {
//    type Out = Macros.View {
//      def i(): scala.Int
//      def i1: scala.Int
//    }
//  }
//
//  (new $anon(): Macros.ViewOf[App.SomeDefWithInt])  // <--- HERE!!!
//}
Run Code Online (Sandbox Code Playgroud)

https://github.com/lampepfl/dotty/issues/15566(对于结构细化,即 defs,它们的损失似乎是预期的行为,但类型细化损失可能是一个错误)

所以,至少有一次我们必须使用低级newClass来避免类型归属

(版本4)

trait ViewOf[M <: Product] {
  type Out <: View
}

object ViewOf {
  transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}

  @experimental // because .newClass is @experimental
  def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
    import quotes.reflect.*

    def makeRefinement(parent: TypeRepr, namesAndTypes: List[(String, TypeRepr)]): TypeRepr =
      namesAndTypes.foldLeft(parent) { case (acc, (name, tpe)) => Refinement(acc, name, tpe) }

    def mkNamesAndTypes[mels: Type, mets: Type]: List[(String, TypeRepr)] =
      (Type.of[mels], Type.of[mets]) match {
        case ('[EmptyTuple], '[EmptyTuple]) => Nil
        case ('[mel *: melTail], '[met *: metTail] ) => {
          val name = Type.valueOfConstant[mel].get.toString
          val name1 = name + "1"
          val methodType = MethodType(List("args"))(_ => List(AnnotatedType(TypeRepr.of[Any], '{new scala.annotation.internal.Repeated()}.asTerm)), _ => TypeRepr.of[met])
          val methodType1 = ByNameType(TypeRepr.of[met])
          (name, methodType) :: (name1, methodType1) :: mkNamesAndTypes[melTail, metTail]
        }
      }

    val namesAndTypes = Expr.summon[Mirror.ProductOf[M]].get match {
      case '{ $m: Mirror.ProductOf[M] { type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
        mkNamesAndTypes[mels, mets]
    }

    val refinement = makeRefinement(TypeRepr.of[View], namesAndTypes)

    def newType(cls: Symbol, name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags, privateWithin: Symbol = Symbol.noSymbol): Symbol = {
      given dotty.tools.dotc.core.Contexts.Context =
        quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
      import dotty.tools.dotc.core.Decorators.toTypeName
      dotty.tools.dotc.core.Symbols.newSymbol(
        cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
        name.toTypeName,
        flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
        tpe.asInstanceOf[dotty.tools.dotc.core.Types.Type],
        privateWithin.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
      ).asInstanceOf[Symbol]
    }

    val viewOfImplDecls = (cls: Symbol) => List(newType(cls, "Out",
      TypeBounds(refinement, refinement),
      Flags.Override))

    val viewOfTypeTree = TypeTree.of[ViewOf[M]]
    val viewOfImplParents = List(TypeTree.of[AnyRef], viewOfTypeTree)

    val viewOfImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewOfImpl", viewOfImplParents.map(_.tpe), viewOfImplDecls, selfType = None)
    val outSymb = viewOfImplCls.declaredType("Out").head

    val outTypeDef = TypeDef(outSymb)
    val viewOfImplClsDef = ClassDef(viewOfImplCls, viewOfImplParents, body = List(outTypeDef))
      // this would be an extra type ascription to be avoided
    // val newViewOfImpl = Typed(Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil), TypeTree.of[ViewOf[M]])
    val newViewOfImpl = Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil)

    val res = Block(List(viewOfImplClsDef), newViewOfImpl).asExprOf[ViewOf[M]]
    println(res.show + "=" + res.asTerm.show(using Printer.TreeStructure))
    res
  }
}
Run Code Online (Sandbox Code Playgroud)