一篇入门 — Scala 宏

前情回顾

上一节, 我简单的说了一下反射的基本概念以及运行时反射的用法, 同时简单的介绍了一下编译原理知识, 其中我感觉最为的地方, 就属泛型的几种使用方式了.
而最抽象的概念, 就是对于符号和抽象树的这两个概念的理解.

现在回顾一下泛型的几种进阶用法:

  • 上界 <:
  • 下界 >:
  • 视界 <%
  • 边界 :
  • 协变 +T
  • 逆变 -T

现在想想, 既然已经有了泛型了, 还要这几个功能干嘛呢? 其实可以类比一下, 之前没有泛型, 而为什么引入泛型呢?

当然是为了代码更好的服用. 想象一下, 本来一个方法没有入参, 但通过参数, 可以减少很多相似代码.

同理, 泛型是什么, generics. 又叫什么, 类型参数化. 本来方法的入参只能接受一种类型的参数, 加入泛型后, 可以处理多种类型的入参.

顺着这条线接着往下想, 有了逆变和协变, 我们让泛型的包装类也有了类继承关系, 有了继承的层级关系, 方法的处理能力又会大大增加.

泛型, 并不神奇, 只是省略了一系列代码, 而且引入泛型还会导致泛型擦除, 以及一系列的隐患. 而类型擦除其实也是为了兼容更早的语言, 我们束手无策.
但泛型在设计上实现的数据和逻辑分离, 却可以大大提高程序代码的简洁性和可读性, 并提供可能的编译时类型转换安全检测功能. 所以在可以使用泛型的地方我们还是推荐的.

编译时反射

上篇文章已经介绍过, 编译器反射也就是在Scala的表现形式, 就是我们本篇的重点 宏(Macros).

Macros 能做什么呢?

直白一点, 宏能够

Code that generates code

还记得上篇文章中, 我们提到的AST(abstract syntax tree, 抽象语法树)吗? Macros 可以利用 compiler plugincompile-time 操作 AST, 从而实现一些为所以为的...任性操作

所以, 可以理解宏就是一段在编译期运行的代码, 如果我们可以合理的利用这点, 就可以将一些代码提前执行, 这意味着什么, 更早的(compile-time)发现错误, 从而避免了 run-time错误. 还有一个不大不小的好处, 就是可以减少方法调用的堆栈开销.

是不是很吸引人, 好, 开始Macros的盛宴.

黑盒宏和白盒宏

黑盒和白盒的概念, 就不做过多介绍了. 而Scala既然引用了这两个单词来描述宏, 那么两者区别也就显而易见了. 当然, 这两个是新概念, 在2.10之前, 只有一种宏, 也就是白盒宏的前身.

官网描述如下:
Macros that faithfully follow their type signatures are called blackbox macros as their implementations are irrelevant to understanding their behaviour (could be treated as black boxes).
Macros that can‘t have precise signatures in Scala‘s type system are called whitebox macros (whitebox def macros do have signatures, but these signatures are only approximations).

我怕每个人的理解不一样, 所以先贴出了官网的描述, 而我的理解呢, 就是我们指定好返回类型的Macros就是黑盒宏, 而我们虽然指定返回值类型, 甚至是以c.tree定义返回值类型, 而更加细致的具体类型, 即真正的返回类型可以在宏中实现的, 我们称为白盒宏.

可能还是有点绕哈, 我举个例子吧. 在此之前, 先把二者的位置说一下:

2.10

  • scala.reflect.macros.Context

2.11 +

  • scala.reflect.macros.blackbox.Context
  • scala.reflect.macros.whitebox.Context

黑盒例子

import scala.reflect.macros.blackbox

object Macros {
    def hello: Unit = macro helloImpl

    def helloImpl(c: blackbox.Context): c.Expr[Unit] = {
        import c.universe._
        c.Expr {
              Apply(
                    Ident(TermName("println")),
                    List(Literal(Constant("hello!")))
              )
        }
    }
}

但是要注意, 黑盒宏的使用, 会有四点限制, 主要方面是

  • 类型检查
  • 类型推到
  • 隐式推到
  • 模式匹配

这里我不细说了, 有兴趣可以看看官网: https://docs.scala-lang.org/overviews/macros/blackbox-whitebox.html

白盒例子

import scala.reflect.macros.blackbox

object Macros {
    def hello: Unit = macro helloImpl

    def helloImpl(c: blackbox.Context): c.Tree = {
      import c.universe._
      c.Expr(q"""println("hello!")""")
    }
}

Using macros is easy, developing macros is hard.

了解了Macros的两种规范之后, 我们再来看看它的两种用法, 一种和C的风格很像, 只是在编译期将宏展开, 减少了方法调用消耗. 还有一种用法, 我想大家更熟悉, 就是注解, 将一个宏注解标记在一个类, 方法, 或者成员上, 就可以将所见的代码, 通过AST变成everything, 不过, 请不要变的太离谱.

Def Macros

方法宏, 其实之前的代码中, 已经见识过了, 没什么稀奇, 但刚才的例子还是比较简单的, 如果我们要传递一个参数, 或者泛型呢?

看下面例子:

object Macros {
    def hello2[T](s: String): Unit = macro hello2Impl[T]

    def hello2Impl[T](c: blackbox.Context)(s: c.Expr[String])(ttag: c.WeakTypeTag[T]): c.Expr[Unit] = {
        import c.universe._
        c.Expr {
            Apply(
                Ident(TermName("println")),
                List(
                    Apply(
                        Select(
                            Apply(
                                Select(
                                    Literal(Constant("hello ")),
                                    TermName("$plus")
                                ),
                                List(
                                    s.tree
                                )
                            ),
                            TermName("$plus")
                        ),
                        List(
                            Literal(Constant("!"))
                        )
                    )
                )
            )
        }
    }
}

和之前的不同之处, 暴露的方法hello2主要在于多了参数s和泛型T, 而hello2Impl实现也多了两个括号

  • (s: c.Expr[String])
  • (ttag: c.WeakTypeTag[T])

我们来一一讲解

c.Expr

这是Macros的表达式包装器, 里面放置着类型String, 为什么不能直接传String呢?
当然是不可以了, 因为宏的入参只接受Expr, 调用宏传入的参数也会默认转为Expr.

这里要注意, 这个(s: c.Expr[String])的入参名必须等于hello2[T](s: String)的入参名

WeakTypeTag[T]

记得上一期已经说过的TypeTagClassTag.

scala> val ru = scala.reflect.runtime.universe
ru @ 6d657803: scala.reflect.api.JavaUniverse = [email protected]

scala> def foo[T: ru.TypeTag] = implicitly[ru.TypeTag[T]]
foo: [T](implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.TypeTag[T]

scala> foo[Int]
res0 @ 7eeb8007: reflect.runtime.universe.TypeTag[Int] = TypeTag[Int]

scala> foo[List[Int]]
res1 @ 7d53ccbe: reflect.runtime.universe.TypeTag[List[Int]] = TypeTag[scala.List[Int]]

这都没有问题, 但是如果我传递一个泛型呢, 比如这样:

scala> def bar[T] = foo[T] // T is not a concrete type here, hence the error
<console>:26: error: No TypeTag available for T
       def bar[T] = foo[T]
                       ^

没错, 对于不具体的类型(泛型), 就会报错了, 必须让T有一个边界才可以调用, 比如这样:

scala> def bar[T: TypeTag] = foo[T] // to the contrast T is concrete here
                                    // because it's bound by a concrete tag bound
bar: [T](implicit evidence$1: reflect.runtime.universe.TypeTag[T])reflect.runtime.universe.TypeTag[T]

但, 有时我们无法为泛型提供边界, 比如在本章的Def Macros中, 这怎么办? 没关系, 杨总说过:

任何计算机问题都可以通过加一层中间件解决.

所以, Scala引入了一个新的概念 => WeakTypeTag[T], 放在TypeTag之上, 之后可以

scala> def foo2[T] = weakTypeTag[T]
foo2: [T]=> reflect.runtime.universe.WeakTypeTag[T]

无须边界, 照样使用, 而TypeTag就不行了.

scala> def foo[T] = typeTag[T]
<console>:15: error: No TypeTag available for T
       def foo[T] = typeTag[T]

有兴趣请看
https://docs.scala-lang.org/overviews/reflection/typetags-manifests.html

Apply

在前面的例子中, 我们多次看到了Apply(), 这是做什么的呢?
我们可以理解为这是一个AST构建函数, 比较好奇的我看了下源码, 搜打死乃.

class ApplyExtractor{
    def apply(fun: Tree, args: List[Tree]): Apply = {
        ???
    }
}

看着眼熟不? 没错, 和ScalaList[+A]的构建函数类似, 一个延迟创建函数. 好了, 先理解到这.

Ident

定义, 可以理解为Scala标识符的构建函数.

Literal(Constant("hello "))

文字, 字符串构建函数

Select

选择构建函数, 选择的什么呢? 答案是一切, 不论是选择方法, 还是选择类. 我们可以理解为.这个调用符. 举个例子吧:

scala> showRaw(q"scala.Some.apply")
res2: String = Select(Select(Ident(TermName("scala")), TermName("Some")), TermName("apply"))

还有上面的例子:
"hello ".$plus(s.tree)

Apply(
    Select(
        Literal(Constant("hello ")),
        TermName("$plus")
    ),
    List(
        s.tree
    )
)

源码如下:

class SelectExtractor {
    def apply(qualifier: Tree, name: Name): Select = {
        ???
    }
}

TermName("$plus")

理解TermName之前, 我们先了解一下什么是Names, Names在官网解释是:

Names are simple wrappers for strings.

只是一个简单的字符串包装器, 也就是把字符串包装起来, Names有两个子类, 分别是TermNameTypeName, 将一个字符串用两个子类包装起来, 就可以使用Select 在tree中进行查找, 或者组装新的tree.

官网地址

宏插值器

刚刚就为了实现一个如此简单的功能, 就写了那么巨长的代码, 如果如此的话, 即便Macros 功能强大, 也不易推广Macros. 因此Scala又引入了一个新工具 => Quasiquotes

Quasiquotes 大大的简化了宏编写的难度, 并极大的提升了效率, 因为它让你感觉写宏就像写scala代码一样.

同样上面的功能, Quasiquotes实现如下:

object Macros {
    def hello2[T](s: String): Unit = macro hello2Impl[T]

    def hello2Impl[T](c: blackbox.Context)(s: c.Expr[String])(ttag: c.WeakTypeTag[T]): c.Expr[Unit] = {
        import c.universe._
        val tree = q"""println("hello " + ${s.tree} + "!")"""

        c.Expr(tree)
    }
}

q""" ??? """ 就和 s""" ??? """, r""" ??? """ 一样, 可以使用$引用外部属性, 方便进行逻辑处理.

Macros ANNOTATIONS

宏注释, 就和我们在Java一样, 下面是我写的一个例子:
对于以class修饰的类, 我们也像case class修饰的类一样, 完善toString()方法.

package com.pharbers.macros.common.connecting

import scala.reflect.macros.whitebox
import scala.language.experimental.macros
import scala.annotation.{StaticAnnotation, compileTimeOnly}

@compileTimeOnly("enable macro paradis to expand macro annotations")
final class ToStringMacro extends StaticAnnotation {
    def macroTransform(annottees: Any*): Any = macro ToStringMacro.impl
}

object ToStringMacro {
    def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
        import c.universe._

        val class_tree = annottees.map(_.tree).toList match {
            case q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$parents { $self => ..$stats }" :: Nil =>

                val params = paramss.flatMap { params =>
                    val q"..$trees" = q"..$params"
                    trees
                }
                val fields = stats.flatMap { params =>
                    val q"..$trees" = q"..$params"
                    trees.map {
                        case q"$mods def toString(): $tpt = $expr" => q""
                        case x => x
                    }.filter(_ != EmptyTree)
                }
                val total_fields = params ++ fields

                val toStringDefList = total_fields.map {
                    case q"$mods val $tname: $tpt = $expr" => q"""${tname.toString} + " = " + $tname"""
                    case q"$mods var $tname: $tpt = $expr" => q"""${tname.toString} + " = " + $tname"""
                    case _ => q""
                }.filter(_ != EmptyTree)
                val toStringBody = if(toStringDefList.isEmpty) q""" "" """ else toStringDefList.reduce { (a, b) => q"""$a + ", " + $b""" }
                val toStringDef = q"""override def toString(): String = ${tpname.toString()} + "(" + $toStringBody + ")""""

                q"""
                    $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends ..$parents { $self => ..$stats
                        $toStringDef
                    }
                """

            case _ => c.abort(c.enclosingPosition, "Annotation @One2OneConn can be used only with class")
        }

        c.Expr[Any](class_tree)
    }
}

compileTimeOnly

非强制的, 但建议加上. 官网解释如下:

It is not mandatory, but is recommended to avoid confusion. Macro annotations look like normal annotations to the vanilla Scala compiler, so if you forget to enable the macro paradise plugin in your build, your annotations will silently fail to expand. The @compileTimeOnly annotation makes sure that no reference to the underlying definition is present in the program code after typer, so it will prevent the aforementioned situation from happening.

StaticAnnotation

继承自StaticAnnotation的类, 将被Scala解释器标记为注解类, 以注解的方式使用, 所以不建议直接生成实例, 加上final修饰符.

macroTransform

def macroTransform(annottees: Any*): Any = macro ToStringMacro.impl

对于使用@ToStringMacro修饰的代码, 编译器会自动调用macroTransform方法, 该方法的入参, 是annottees: Any*, 返回值是Any, 主要是因为Scala缺少更细致的描述, 所以使用这种笼统的方式描述可以接受一切类型参数.
而方法的实现, 和Def Macro一样.

impl

def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._
    ???
}

到了Macros的具体实现了. 这里其实和Def Macro也差不多. 但对于需要传递参数的宏注解, 需要按照下面的写法:

final class One2OneConn[C](param_name: String) extends StaticAnnotation {
    def macroTransform(annottees: Any*): Any = macro One2OneConn.impl
}

object One2OneConn {
    def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
        import c.universe._

        // 匹配当前注解, 获得参数信息
        val (conn_type, conn_name) = c.prefix.tree match {
            case q"new One2OneConn[$conn_type]($conn_name)" =>
                (conn_type.toString, conn_name.toString.replace("\"", ""))
            case _ => c.abort(c.enclosingPosition, "Annotation @One2OneConn must provide conn_type and conn_name !")
        }

        ???
    }
}

有几点需要注意的地方:

  1. 宏注解只能操作当前自身注解, 和定义在当前注解之下的注解, 对于之前的注解, 因为已经展开, 所以已经不能操作了.
  2. 如果宏注解生成多个结果, 例如既要展开注解标识的类, 还要直接生成类实例, 则返回结果需要以块(Block)包起来.
  3. 宏注释必须使用白盒宏.

Macro Paradise

Scala 推出了一款插件, 叫做Macro Paradise(宏天堂), 可以帮助开发者控制带有宏的Scala代码编译顺序, 同时还提供调试功能, 这里不做过多介绍, 有兴趣的可以查看官网: Macro Paradise

原文地址:https://www.cnblogs.com/clockq/p/9661639.html

时间: 2024-10-26 06:05:30

一篇入门 — Scala 宏的相关文章

scala 宏

Scala开发团队正在将实验版宏指令加入到即将发行的2.10版中.Scala宏指令提供了编译时元编程的高级形式.Scala宏网站描述道: “宏指令显著简化了代码分析和代码生成,这使得它们成为处理大量现实用例的一种可选工具.传统上涉及编写和维护样板的场合可用宏以简单且易维护的方式实现.因此我们认为宏对于Scala编程语言是一项非常有价值的资产.” Scala的宏指令允许开发者创建方法时以语法树转化的形式实现.这些是标准方法的定义,其在编译期间被显式地转换.举一个简单的例子,如assert方法: i

Scala详解---------快速入门Scala

我无可救药地成为了Scala的超级粉丝.在我使用Scala开发项目以及编写框架后,它就仿佛凝聚成为一个巨大的黑洞,吸引力使我不得不飞向它,以至于开始背离Java.固然Java 8为Java阵营增添了一丝亮色,却是望眼欲穿,千呼万唤始出来.而Scala程序员,却早就在享受lambda.高阶函数.trait.隐式转换等带来的福利了. Java像是一头史前巨兽,它在OO的方向上几乎走到了极致,硬将它拉入FP阵营,确乎有些强人所难了.而Scala则不,因为它的诞生就是OO与FP的混血儿--完美的基因融合

ensemble 的2篇入门 文章

python 篇: http://machinelearningmastery.com/ensemble-machine-learning-algorithms-python-scikit-learn/ http://machinelearningmastery.com/compare-machine-learning-algorithms-python-scikit-learn/ R 篇: http://machinelearningmastery.com/machine-learning-e

[实战篇入门]02-POI简单创建Excel

周日的小讲堂要讲到这里,趁中午时间写点东西,记录昨天晚上完成的东西,在这里只是简单的介绍如何创建对于样式问题,我不过多的说,因为之后的教程会使用模版方式搞定! 在学习这段代码的时候,希望各位访问Apache的官方网站,里面有快速入门的教程,这是最好的入门方式 --> http://poi.apache.org/spreadsheet/quick-guide.html 这里我建议使用官方文档提高的教程,方便操作 代码如下 1 package com.shxt.poi.write; 2 3 impo

【Scala篇】--Scala中Trait、模式匹配、样例类、Actor模型

一.前述 Scala Trait(特征) 相当于 Java 的接口,实际上它比接口还功能强大. 模式匹配机制相当于java中的switch-case. 使用了case关键字的类定义就是样例类(case classes),样例类是种特殊的类. Actor相当于Java中的多线程. 二.具体阐述 trait特性 1.概念理解 Scala Trait(特征) 相当于 Java 的接口,实际上它比接口还功能强大. 与接口不同的是,它还可以定义属性和方法的实现. 一般情况下Scala的类可以继承多个Tra

elasticsearch 第一篇(入门篇)

介绍 elasticsearch是一个高效的.可扩展的全文搜索引擎 基本概念 Near Realtime(NRT): es是一个接近实时查询平台,意味从存储一条数据到可以索引到数据时差很小,通常在1s内 Cluster: es是一个分布式.可扩展的平台, 可由一个或多个服务器通过定义的cluster.name(默认为elasticsearch)标识共建同一个集群 Node: 通常一台服务器上部署一台es node,作为集群的一部分,用于数据的存储和提供搜索功能,在一个集群中节点通过node.na

java nio网络编程服务篇入门

服务端写完了,现在写一个客户端,对于客户端,我考虑使用nio或阻塞socket都可以. 使用nio的客户端: 1 /** 2 * 初始化网络连接 3 */ 4 public void run() { 5 6 // 开启网络连接 7 try { 8 channel = SocketUtils.connect("127.0.0.1",8080); 9 selector = Selector.open(); 10 channel.register(selector, SelectionKey

Python基础篇(入门)

一.Python 简介.特性.应用 Python是一门计算机编程语言,Python是一门计算机编程语言,它是由荷兰人Guido van Rossum在1989年圣诞节期间为了打发无聊的圣诞节而编写的,作为ABC语言的继承 特性: 面向对象.解释型.动态.高级的计算机编程语言,官方定义其是优雅.明确.简单. 开源免费.跨平台.移植性,可以在各种系统上使用 说是容易上手.简单易学.功能强大. 涵盖各种功能的标准库.第三方库.  batteries included (称为内置电池) 让开发者把精力放

JMeter工具基础知识篇-入门知识介绍

转载于金阳光测试 JMeter背景知识介绍: 1)  一个100%的纯Java桌面应用,它的用户界面采用Swing Java API实现 2)  Apache软件基金会下的一个子项目,Google在插件方面的持续支持 3)  自1998年发版以来,一直随着时代的发展在持续改进中,从3.0版本开始测试报告变得非常美观 4)  能满足接口功能自动化.批量数据准备.性能测试等多重需求 5)  直观的图形化操作界面,丰富的结果报告图表,强大而易用 6)  它是独立于平台的工具,在Windows / Li