You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scala宏开发:如何从WeakTypeTag获取完整类型信息生成正确Witness

核心问题解析

1. 为什么泛型参数T1、T2会显示为占位符,而不是具体类型?

这不是编译期类型擦除导致的,根本原因是宏的展开时机和调用位置不对:

  • 宏是在调用点所处的编译作用域即时展开的。你把Type2String[^^[T1, T2]]写在了case class ^^[T1, T2]的类体内部,这个代码位置的编译发生在类的定义阶段。此时T1、T2就是这个类自身声明的两个抽象类型参数,没有绑定任何具体的类型实参,宏通过WeakTypeTag拿到的Type结构里,这两个位置就是抽象类型的符号引用,直接调用toString自然就会输出符号本身的名字"T1"、"T2"。
  • 等到你写val e1 = ^^[Int, String]()实例化类的时候,^^类的代码早已编译完成,对应的宏也已经在类定义阶段展开结束,根本不会在实例化阶段重新执行宏去捕获Int、String这些具体的类型值。最终拿到的Witness自然就是类定义阶段生成的、带占位符字符串的错误结果。

2. 如何修改代码拿到正确的shapeless.Witness.Aux[String("Int ^^ String")]?

需要从两个方向调整实现:

第一、修正宏内部的代码生成逻辑

你之前的宏实现存在两个bug,哪怕拿到了正确的类型,也生成不出合法的单例Witness:

  • 之前的quasiquote写法q"shapeless.Witness.mkWitness[$str]($str)"中,$str会被解析为代码里的变量引用,不是字符串字面量常量,既不能生成单例的字面量类型,值的位置也不是合法的单例字符串。
  • 拿到Type之后需要先做别名解引用,避免拿到的是类型符号的内部名而非可读的类型描述。

修正后的宏实现示例:

def apply[A: WeakTypeTag]: Tree = {
  // 先解类型别名,拿到完整的带实参的类型结构
  val targetType = weakTypeOf[A].dealias
  // 生成类型的字符串描述
  val strRepr = targetType.toString
  // 显式构造字符串字面量的常量树,保证生成的是单例类型
  val strLiteral = Literal(Constant(strRepr))
  q"shapeless.Witness.mkWitness[${strLiteral.tpe}]($strLiteral)"
}

第二、调整宏的调用位置,保证展开时能拿到具体的类型绑定

不能把宏调用直接放在泛型类的类体里引用类自身的抽象类型参数,否则宏永远只能拿到未绑定的占位符。可以通过两种方式让宏在“类型已经绑定具体实参”的上下文里展开:

  • 方式一:直接在实例化位置调用宏,比如写Type2String[^^[Int, String]],此时宏拿到的就是带完整实参的类型,生成的字符串就是"Int ^^ String"。
  • 方式二:把类内部的wTSelf改成实例级的方法定义,让宏在方法被调用的位置捕获当前类型。比如:
    case class ^^[T1, T2]() {
      // 把val改成def,让宏在实际调用这个方法的位置展开
      final def wTSelf: Witness.Lt[String] = macro Macros.apply[^^[T1, T2]]
    }
    
    当你执行e1.wTSelf的时候,宏展开的位置已经处于e1: ^^[Int, String]的作用域,能拿到绑定了具体Int、String实参的完整类型结构,最终生成的Witness的单例值就会是"Int ^^ String"。

注意:如果保留原来的final val wTSelf = ...的写法,这个常量会在类构造时初始化,对应的宏还是会在类定义阶段展开,依然会拿到占位符的结果。


内容的提问来源于stack exchange,提问作者tribbloid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 20:45:04