如何在Dotty中使用宏生成类?

10

在Dotty(Scala 3)中,使用宏生成新类是否可行?

Zlaja


1
宏从来不是用来创建全新类型(包括类)的。最多你可以创建一个针对某些参数进行调整的宏生成的类实例。我认为宏注解和/或编译器插件可以用于将新的类主体插入到已注释类的伴生对象中(这不是一个好主意),但仅此而已。如果您想要生成类,请使用代码生成器。 - Mateusz Kubuszok
在Dotty的@main注解中,我只找到了以下类:main extends scala.annotation.Annotation {},那么,宏注解是否需要编译器插件才能运行呢? - zlaja
1
@zlaja @main 不是宏注解,而是由Dotty编译器管理的单独注解。目前,您无法在Dotty中创建此类自定义注解,除非使用编译器插件。 - Dmytro Mitin
@zlaja 我更新了我的答案,提供了在Scala 3.3.0中添加宏注释的示例。 - Dmytro Mitin
2个回答

10

目前在Dotty中只有(某种程度上){{link1:def宏}}。 目前没有(某种程度上的){{link2:宏注释}},可以生成新成员、新类等。

要生成新成员、新类等,您可以使用

让我提醒您,即使在Scalac中生成新成员、新类等功能也不是从一开始就存在的。这样的功能(宏注释)出现在Scalac的Macro Paradise编译器插件中。

我不能排除有人会为Dotty编写类似Macro Paradise的东西。现在为时尚早,因为Dotty目前只有功能冻结,甚至语言语法(例如)和标准库也还在不断变化(还有一些库正在测试它们与Dotty的兼容性,例如目前没有Scalaz/Cats)。


例如,在Scala 2中,Simulacrum使用宏注释,而在Scala 3中,Simulacrum-Scalafix是作为Scalafix规则实现的。

https://github.com/typelevel/simulacrum-scalafix

https://index.scala-lang.org/typelevel/simulacrum-scalafix/simulacrum-scalafix/0.5.0?target=_2.12

一个更多的用例:Breeze使用sbt插件和Scalameta进行源代码生成。

https://github.com/scalanlp/breeze

https://github.com/scalanlp/breeze/blob/master/DEVELOP.md

https://github.com/dlwh/sbt-breeze-expand-codegen


更新。 现在我们可以使用Scala 3 (def)宏生成一个内部类:Scala 3宏实现方法重写


更新(2023年3月)。从Scala 3.3.0-RC2开始,出现了宏注释(Nicolas Stucki实现)。

https://github.com/lampepfl/dotty/releases/tag/3.3.0-RC2讨论

[概念验证] 通过重写宏注释中的错误生成代码https://github.com/lampepfl/dotty/pull/16545

Zhendong Ang. Scala 3的宏注释(硕士论文)https://infoscience.epfl.ch/record/294615

宏注解(第一部分)https://github.com/lampepfl/dotty/pull/16392

宏注解类修改(第二部分)https://github.com/lampepfl/dotty/pull/16454

允许从MacroAnnotations返回类(第三部分)https://github.com/lampepfl/dotty/pull/16534

新定义在宏展开外部不可见。

build.sbt

scalaVersion := "3.3.0-RC3"

这是一个例子。宏注解@genObj生成一个伴生对象,其中包含given tc: TC[A] = new TC[A]@modifyObj修改生成的伴生对象,在内部生成given tc: TC[A] = new TC[A]
import scala.annotation.{MacroAnnotation, experimental}
import scala.quoted.*

object Macros:
  class TC[T]

  @experimental
  class genObj extends MacroAnnotation:
    def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
      import quotes.reflect.*
      tree match
        case ClassDef(className, _, _, _, _) =>
          val modParents = List(TypeTree.of[Object])

          val tpe = TypeRepr.of[TC].appliedTo(List(tree.symbol.typeRef))

          def decls(cls: Symbol): List[Symbol] = List(
            Symbol.newVal(cls, "tc", tpe, Flags.Given, Symbol.noSymbol)
          )

          val mod = Symbol.newModule(Symbol.spliceOwner, className, Flags.EmptyFlags, Flags.EmptyFlags,
            modParents.map(_.tpe), decls, Symbol.noSymbol)
          val cls = mod.moduleClass
          val tcSym = cls.declaredField("tc")
          val tcDef = tpe.asType match
            case '[TC[t]] => ValDef(tcSym, Some('{new TC[t]}.asTerm))
          val (modValDef, modClsDef) = ClassDef.module(mod, modParents, body = List(tcDef))

          val res = List(tree, modValDef, modClsDef)
          println(res.map(_.show))
          res
        case _ =>
          report.errorAndAbort("@genObj can annotate only classes")

  @experimental
  class modifyObj extends MacroAnnotation:
    def transform(using Quotes)(tree: quotes.reflect.Definition): List[quotes.reflect.Definition] =
      import quotes.reflect.*
      tree match
        case ClassDef(name, constr, parents, selfOpt, body) =>

          val tpe = TypeRepr.of[TC].appliedTo(List(tree.symbol.companionClass.typeRef))

          val tcSym = Symbol.newVal(tree.symbol, "tc", tpe, Flags.Given, Symbol.noSymbol)
          val tcDef = tpe.asType match
            case '[TC[t]] => ValDef(tcSym, Some('{ new TC[t] }.asTerm))

          val res = List(ClassDef.copy(tree)(name, constr, parents, selfOpt, body :+ tcDef))
          println(res.map(_.show))
          res
        case _ =>
          report.errorAndAbort("@modifyObj can annotate only classes")

import Macros.{TC, genObj, modifyObj}
import scala.annotation.experimental

@experimental
object App:
  @genObj
  class A

//scalac: List(@Macros.genObj class A(),
//lazy val A: App.A.type = new App.A(),
//object A extends java.lang.Object { this: App.A.type =>
//  val tc: Macros.TC[App.A] = new Macros.TC[App.A]()
//})

import Macros.{TC, genObj, modifyObj}
import scala.annotation.experimental

@experimental
object App:
  class A

  @modifyObj
  object A

//scalac: List(@Macros.modifyObj object A {
//  val tc: Macros.TC[App.A] = new Macros.TC[App.A]()
//})

Scala 3中的宏注解

如何使用Scala 3宏在编译时生成无参构造函数?

Scala 3宏创建枚举


1
除了Simulacrum-scalafix之外,还有一个用例:Breeze使用sbt插件和Scalameta进行源代码生成 https://github.com/scalanlp/breeze https://github.com/scalanlp/breeze/blob/master/DEVELOP.md https://github.com/dlwh/sbt-breeze-expand-codegen - Dmytro Mitin
1
https://contributors.scala-lang.org/t/sponsoring-work-on-scala-3-macro-annotations/5658在Scala 3中,宏注解是一个非常强大的功能,但是目前还没有完全实现。这个项目旨在为Scala 3开发一个新的宏注解系统,以便更好地支持编译时元编程。我们正在寻找赞助商来资助这个项目的开发。https://users.scala-lang.org/t/macro-annotations-replacement-in-scala-3/7374Scala 3中的宏注解将被替换为一个新的宏系统。这个新系统将提供更好的性能和可维护性,并且将更好地支持Scala 3的类型系统。如果您正在使用宏注解,请考虑迁移到新的宏系统。 - Dmytro Mitin
1
https://github.com/lampepfl/dotty/pull/16392 https://infoscience.epfl.ch/record/294615 - Dmytro Mitin
1
https://contributors.scala-lang.org/t/whitebox-macros-in-scala-3-are-possible-after-all/5014 - Dmytro Mitin
1
https://contributors.scala-lang.org/t/pre-sip-export-macros/6168 https://github.com/littlenag/dotty/tree/export-macro https://gist.github.com/littlenag/d0c9dfddeb9002684c6effef18c2ec5e - Dmytro Mitin
显示剩余6条评论

6

您可以制作一个透明宏,返回一个结构类型,用于生成任何您希望的valdef类型。

这里是一个示例:当使用Product类型调用方法props时,将创建一个对象,并将第一个Product元素名称作为String val

case class User(firstName: String, age: Int)

// has the type of Props { val firstName: String }
val userProps = props[User]

println(userProps.firstName) // prints "prop for firstName"
println(userProps.lastName) // compile error

实现起来有点棘手,但并不太难:

import scala.compiletime.*
import scala.quoted.*
import scala.deriving.Mirror

class Props extends Selectable:
  def selectDynamic(name: String): Any =
    "prop for " + name

transparent inline def props[T] =
  ${ propsImpl[T] }

private def propsImpl[T: Type](using Quotes): Expr[Any] =
  import quotes.reflect.*

  Expr.summon[Mirror.ProductOf[T]].get match
    case '{ $m: Mirror.ProductOf[T] {type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
      Type.of[mels] match
        case '[mel *: melTail] =>
          val label = Type.valueOfConstant[mel].get.toString

          Refinement(TypeRepr.of[Props], label, TypeRepr.of[String]).asType match
            case '[tpe] =>
              val res = '{
                val p = Props()
                p.asInstanceOf[tpe]
              }
              println(res.show)
              res

通过递归,你可以不断地完善Refinement(因为Refinement<:TypeRepr),直到所有内容都构建完成。

话虽如此,使用transparent inline甚至是Scala 2宏注解生成新类型会使IDE很难支持自动完成。因此,如果可能的话,我建议使用标准Typeclass派生

你甚至可以推导出标准特质的默认行为:

trait SpringDataRepository[E, Id]:
  def findAll(): Seq[E]

trait DerivedSpringDataRepository[E: Mirror.ProductOf, Id]:
  def findAll(): Seq[E] = findAllDefault[E, Id]()
  
private inline def findAllDefault[E, Id](using m: Mirror.ProductOf[E]): Seq[E] =
  findAllDefaultImpl[E, m.MirroredLabel, m.MirroredElemLabels]()

private inline def findAllDefaultImpl[E, Ml, Mels](columns: ArrayBuffer[String] = ArrayBuffer()): Seq[E] =
  inline erasedValue[Mels] match
    case _: EmptyTuple =>
      // base case
      println("executing.. select " + columns.mkString(", ") + " from " + constValue[Ml])
      Seq.empty[E]
    case _: (mel *: melTail) =>
      findAllDefaultImpl[E, Ml, melTail](columns += constValue[mel].toString) 
      

然后,用户只需要扩展 DerivedSpringDataRepository 与他们的 Product 类型:

case class User(id: Int, first: String, last: String)

class UserRepo extends DerivedSpringDataRepository[User, Int]

val userRepo = UserRepo()

userRepo.findAll() // prints "executing.. select id, first, last from User"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接