🍁 Scala3 macro & tasty 获取 TypeClass 描述信息

1.概述

Scala3的Macro基于其引入的新的抽象类型结构 tasty,即:Typed Abstract Syntax Trees,其可以在编译之后,保留方法和类上的类型信息,以方便Scala3的 MetaPrograming 编程。

本文尝试提供一个 Describer 工具在运行期获取当前类型的各种信息,比如 TypeTree,TypeRepr, Symbol 等等,以方便我们在学习 Macro 时,对 tasty 结构认识和加深印象。

2.实践 describe

定义 macro 方法 describe,并根据传递的 ShowType 类型返回对应的需要的类型信息。

object Describer:
 enum ShowType:
    case TYPE_TREE
    case TYPE_REPR
    case OTHER

  inline def describe[T](showType: ShowType): String = ${describeImpl[T]('showType)}

如何去实现 describeImpl 呢?以下是我在编码过程中的几个版本和自己的一些思路,前面的版本完成后,很显然的编译无法通过,于是催生了修改的版本,最终编译成功,并加深自己的学习。

2.1 v1 代码实现

object Describer {

  inline def describe[T](showType: ShowType): String = ${describeImpl[T]('showType)}

  def describeImpl[T: Type](showType: Expr[ShowType])(using Quotes): Expr[String] = {
    import quotes.reflect.*
     
   val st = ${showType}
    st match
          case ShowType.TYPE_TREE ⇒
            val tpt = TypeTree.of[T]
            Literal(StringConstant(tpt.show)).asExprOf[String]
          case ShowType.TYPE_REPR ⇒
            val tpr = TypeRepr.of[T]
            Literal(StringConstant(tpr.dealias.show)).asExprOf[String]
          case ShowType.OTHER ⇒
            '{"Not supported."}
  }
}
error.png

提示报错:Splice ${...} outside quotes '{...} or inline method,即:${} 操作不能在 '{} quotos 操作外部进行(只有一种场景 splices 可以在 quotes 外面,就是 macro 方法入口类,如有疑问,欢迎指正)。

2.2 v2 代码实现

修改代码,使用 quotos 去 wrap 整段代码,这样 splice 就可以使用了。

object Describer {

  enum ShowType:
    case TYPE_TREE
    case TYPE_REPR
    case OTHER

  inline def describe[T](showType: ShowType): String = ${describeImpl[T]('showType)}

  def describeImpl[T: Type](showType: Expr[ShowType])(using Quotes): Expr[String] = {
    import quotes.reflect.*
    //showType.asTerm 可以拿到 ExprImpl 下的 trees 信息
    '{
      val showType1 = ${showType}
      showType1 match
        case ShowType.TYPE_TREE ⇒
          val tpt = TypeTree.of[T]
          tpt.show
        case ShowType.TYPE_REPR ⇒
          val tpr = TypeRepr.of[T]
          tpr.dealias.show
        case ShowType.OTHER ⇒
          "Not supported."
    }
  }
}
error.png
access to parameter evidence$1 from wrong staging level:
            - the definition is at level 0,
            - but the access is at level 1.

提示以上报错,以上 quote 里想通过 $showType 来将 Expr[ShowType] 转换为 ShowType,但是 scala3 的 macro 是在编译期间运行的,编译器无法获取到 ShowType ,只能 获取到 ShowType 的 tasty 结构的信息,即 Expr[ShowType]。所以当尝试在 macro 方法内对 Expr[ShowType] 转化时就会报以上错误。

主要原因是不能在 quotes level 层去获取到 ShowType 信息(存疑)?

2.3 v3 代码实现

通过在 quotes 里使用 scala3 新特性 tasty 的类型结构特性来进行编码和匹配,这一版成功运行。

object Describer {

  inline def describe[T](showType: ShowType): String = ${describeImpl[T]('showType)}

  def describeImpl[T: Type](showType: Expr[ShowType])(using Quotes): Expr[String] = {
    import quotes.reflect.*
    //showType.asTerm 可以拿到 ExprImpl 下的 trees 信息
    showType.asTerm match
      case Inlined(_,_,Ident(content)) ⇒
        ShowType.valueOf(content) match
          case ShowType.TYPE_TREE ⇒
            val tpt = TypeTree.of[T]
            Literal(StringConstant(tpt.show)).asExprOf[String]
          case ShowType.TYPE_REPR ⇒
            val tpr = TypeRepr.of[T]
            Literal(StringConstant(tpr.dealias.show)).asExprOf[String]
          case ShowType.OTHER ⇒
            '{"Not supported."}
      case _ ⇒ '{"Not supported."}
  }
}

22.8.18 更新: 上述代码中,通过将 tpt.show 包装为 Expr[String] 的代码可以进行简化:

//Literal(StringConstant(tpr.dealias.show)).asExprOf[String]
Expr(tpr.dealias.show)
//   '{"Not supported."}
Expr("Not supported.")

测试代码:

  @main def showTypeOf(): Unit = {
    //1
    var str = Describer.describe[User]((ShowType.TYPE_TREE))
    println(s"describe str: $str")
    //2
    str = Describer.describe[User]((ShowType.TYPE_REPR))
    println(s"describe str: $str")
  }

2.4 总结

上文中的 ShowType 可以进行扩展,我们可以去获取更多的类型信息。我们实现的 describeImpl 信息会在编译期进行执行,而编译期中的代码类型和结构,都可以通过 quotes api 来进行获取 和进行 match。
总结原则:

    1. splice 除了在 macro 入口可以在 quotes 之外外,其他情况,都需要在 quotes 以内。
    1. staging level,macro实现是在编译期运行的,编译期间无法将 Expr[T] 在实现内部直接转换为 T,否则就会报 staging level 相关异常。
    1. 在 quotes中,通过 match case 方式提取出类型信息中需要的 part 部分。

3.实践 describeImpl 添加 using 自己写的 given

scala 库有提供 FromExpr trait.

trait FromExpr[T] {

  /** Return the value of the expression.
   *
   *  Returns `None` if the expression does not represent a value or possibly contains side effects.
   *  Otherwise returns the `Some` of the value.
   */
  def unapply(x: Expr[T])(using Quotes): Option[T]

}

我们在代码中写个实现,通过 given with 的形式给出:

  given fromExpr[T]: FromExpr[T] with
    override def unapply(expr: Expr[T])(using Quotes): Option[T] =
      import quotes.reflect.*
      @tailrec
      def rec(tree: Term): Option[T] =
        tree match
          case Block(stats, e) ⇒
            if stats.isEmpty then rec(e) else None
          case Inlined(_, bindings, e) ⇒
            if bindings.isEmpty then rec(e) else None
          case Typed(e, _) ⇒ rec(e)
          case _ ⇒
            tree.tpe.widenTermRefByName match
              case ConstantType(c) ⇒ Some(c.value.asInstanceOf[T])
              case _ ⇒ None

  rec(expr.asTerm)

然后在 2.x 中的 describeImpl 引入 FromExpr,代码如下:

  def describeImpl[T: Type](showType: Expr[ShowType])(using Quotes,FromExpr[T]): Expr[String] =
    import quotes.reflect.*
      val exprOpt = showType.value
      println(s"exprOpt: $exprOpt")
      showType.asTerm match
        case Inlined(_, _, Ident(content)) ⇒
          ShowType.valueOf(content) match
            case ShowType.TYPE_TREE ⇒
              val tpt = TypeTree.of[T]
              Expr(tpt.show)
            case ShowType.TYPE_REPR ⇒
              val tpr = TypeRepr.of[T]
              Expr(tpr.show)
            case ShowType.OTHER ⇒
              Expr("Not supported.")
        case _ ⇒ Expr("Not supported.")

编译器会报如下错误,提示畸形的 macro 参数。

 inline def describe[T](showType: ShowType): String = ${describeImpl[T]('showType)}
                                                                                                                                      ^
Malformed macro parameter: com.maple.scala3.macros2.Describer.fromExpr[T]

为什么会如此?因为在 describeImpl 逻辑中无法找到 FromExpr 的 given 实例,比较疑惑为什么找不到?

通过修改 describeImpl 实现,在其方法里面增加内部方法,在方法内部可以获取到 FromExpr given 实例,代码如下,可以编译通过。

  def describeImpl[T: Type](showType: Expr[ShowType])(using Quotes): Expr[String] =
    import quotes.reflect.*
    def func(showType: Expr[ShowType])  (using Quotes,FromExpr[T]) =
      //showType.asTerm 可以拿到 ExprImpl 下的 trees 信息
      val exprOpt = showType.value
      println(s"exprOpt: $exprOpt")

      showType.asTerm match
        case Inlined(_, _, Ident(content)) ⇒
          ShowType.valueOf(content) match
            case ShowType.TYPE_TREE ⇒
              val tpt = TypeTree.of[T]
              Expr(tpt.show)
            case ShowType.TYPE_REPR ⇒
              val tpr = TypeRepr.of[T]
              Expr(tpr.show)
            case ShowType.OTHER ⇒
              Expr("Not supported.")
        case _ ⇒ Expr("Not supported.")
    func(showType)
  • 定义内部函数 func
  • using FromExpr[T] 挪到内部函数 func 内
  • 在最后调用 func, 这时可以将 FromExpr[T] 给找到。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容