Scala 3如何通过宏消除Trait实现类的重复构造代码?
问题背景
有一个Scala trait WithPayload,所有实现它的case class都必须在构造函数里重复声明并初始化trait的字段,代码冗余问题明显:
sealed trait WithPayload: def description: String def payload1: Int def payload2: Long // 必须手动列出所有WithPayload字段 final case class Foo( override val payload1: Int, override val payload2: Long ) extends WithPayload: override def description = "foo" // 同样要重复写一遍字段声明 final case class Bar( override val payload1: Int, override val payload2: Long ) extends WithPayload: override def description = "bar"
希望通过Scala宏消除这种重复,实现类似C++宏的效果:
#define EXTENDS_WITH_PAYLOAD ( \ override val payload1: Int, \ override val payload2: Long \ ) extends WithPayload
从而写出简洁的代码:
final case class Foo EXTENDS_WITH_PAYLOAD: override def description = "foo" final case class Bar EXTENDS_WITH_PAYLOAD: override def description = "bar"
解决方案
当然可以,在Scala 3中,你可以通过宏注解或者inline元编程实现这个需求,以下是两种具体实现方式:
方案一:宏注解(最贴近需求语法)
宏注解能在编译期修改类定义,自动为case class添加构造参数和trait继承声明,完全贴合你想要的简洁语法。
1. 实现宏注解
import scala.quoted.* // 定义注解类 class extendsWithPayload extends scala.annotation.Annotation: inline def apply(defn: Any): Any = ${ ExtendsWithPayloadMacro.impl(defn) } // 宏实现逻辑 object ExtendsWithPayloadMacro: def impl(defn: Expr[Any])(using Quotes): Expr[Any] = import quotes.reflect.* defn match // 匹配无参的final case class定义 case '{ final case class $className() extends $parent: $body } => // 构造需要添加的payload字段参数 val payload1Param = ValDef.overloaded( name = "payload1", tpt = TypeTree.of[Int], flags = Flags.Val | Flags.Override ) val payload2Param = ValDef.overloaded( name = "payload2", tpt = TypeTree.of[Long], flags = Flags.Val | Flags.Override ) // 修改原类的构造参数和父类 val updatedClass = ClassDef.copy(className.asInstanceOf[Ident].symbol.asClass)( name = className.asInstanceOf[Ident].name, tparams = Nil, constr = DefDef.copy(Symbol.spliceOwner.primaryConstructor)( name = "<init>", paramss = List(List(payload1Param, payload2Param)), tpt = TypeTree.of[Unit], rhs = None ), parents = List(TypeTree.of[WithPayload]), self = None, body = body.asInstanceOf[Term].asInstanceOf[Block].stats ) // 返回修改后的类定义表达式 Expr(updatedClass) // 处理不符合要求的注解使用场景 case _ => report.error("@extendsWithPayload 仅适用于无参final case class") defn
2. 使用注解简化代码
现在你可以像这样写代码,编译期宏会自动展开成完整的case class定义:
@extendsWithPayload final case class Foo: override def description = "foo" @extendsWithPayload final case class Bar: override def description = "bar"
方案二:inline元编程(生成式定义)
如果能接受稍不同的调用语法,也可以用Scala 3的inline特性实现类的生成:
import scala.quoted.* inline def createPayloadCaseClass[T](inline desc: String): (Int, Long) => T = ${ createPayloadCaseClassImpl[T](desc) } object createPayloadCaseClassImpl: def createPayloadCaseClassImpl[T](desc: Expr[String])(using Quotes, Type[T]): Expr[(Int, Long) => T] = import quotes.reflect.* val className = TypeRepr.of[T].typeSymbol.name // 定义case class的字段和构造函数 val payload1Sym = Symbol.newVal(Symbol.spliceOwner, "payload1", TypeRepr.of[Int], Flags.Val | Flags.Override, Symbol.noSymbol) val payload2Sym = Symbol.newVal(Symbol.spliceOwner, "payload2", TypeRepr.of[Long], Flags.Val | Flags.Override, Symbol.noSymbol) // 构造case class定义 val classDef = ClassDef( Symbol.newClass(Symbol.spliceOwner, className, Nil, selfType = None, Flags.Case | Flags.Final), tparams = Nil, constr = DefDef(Symbol.newMethod(Symbol.spliceOwner, "<init>", Nil, List(List(payload1Sym, payload2Sym)), TypeRepr.of[Unit], Flags.Method), paramss = List(List(ValDef(payload1Sym), ValDef(payload2Sym))), tpt = TypeTree.of[Unit], rhs = None ), parents = List(TypeTree.of[WithPayload]), self = None, body = List( // 实现description方法 DefDef(Symbol.newMethod(Symbol.spliceOwner, "description", Nil, Nil, TypeRepr.of[String], Flags.Method), paramss = Nil, tpt = TypeTree.of[String], rhs = Some(desc.asTerm) ) ) ) // 生成伴生对象的apply方法引用 classDef.symbol.companionModule Expr.betaReduce('{ ${Expr(classDef.symbol.companionModule)}.apply })
调用示例
// 生成Foo类的构造器 val fooBuilder = createPayloadCaseClass[Foo]("foo") val foo = fooBuilder(1, 2L) val barBuilder = createPayloadCaseClass[Bar]("bar") val bar = barBuilder(3, 4L)
注意事项
- 上述方案基于Scala 3实现,Scala 2的宏系统语法差异较大,无法直接复用;
- 宏注解需要正确配置编译期依赖,通常建议将宏代码放在单独的模块中;
- 后续如果
WithPayload的字段有修改,只需更新宏实现,所有使用注解的类会自动同步,无需逐个修改。
内容的提问来源于stack exchange,提问作者Nil Admirari
相关产品推荐
相关产品推荐

