一篇入門 — Scala 宏

前情回顧

上一節, 我簡單的說了一下反射的基本概念以及運行時反射的用法, 同時簡單的介紹了一下編譯原理知識, 其中我感受最爲的地方, 就屬泛型的幾種使用方式了.
而最抽象的概念, 就是對於符號和抽象樹的這兩個概念的理解.html

如今回顧一下泛型的幾種進階用法:api

  • 上界 <:
  • 下界 >:
  • 視界 <%
  • 邊界 :
  • 協變 +T
  • 逆變 -T

如今想一想, 既然已經有了泛型了, 還要這幾個功能幹嗎呢? 其實能夠類比一下, 以前沒有泛型, 而爲何引入泛型呢?安全

固然是爲了代碼更好的服用. 想象一下, 原本一個方法沒有入參, 但經過參數, 能夠減小不少類似代碼.app

同理, 泛型是什麼, generics. 又叫什麼, 類型參數化. 原本方法的入參只能接受一種類型的參數, 加入泛型後, 能夠處理多種類型的入參.ide

順着這條線接着往下想, 有了逆變和協變, 咱們讓泛型的包裝類也有了類繼承關係, 有了繼承的層級關係, 方法的處理能力又會大大增長.函數

泛型, 並不神奇, 只是省略了一系列代碼, 並且引入泛型還會致使泛型擦除, 以及一系列的隱患. 而類型擦除其實也是爲了兼容更早的語言, 咱們一籌莫展.
但泛型在設計上實現的數據和邏輯分離, 卻能夠大大提升程序代碼的簡潔性和可讀性, 並提供可能的編譯時類型轉換安全檢測功能. 因此在可使用泛型的地方咱們仍是推薦的.工具

編譯時反射

上篇文章已經介紹過, 編譯器反射也就是在Scala的表現形式, 就是咱們本篇的重點 宏(Macros).ui

Macros 能作什麼呢?

直白一點, 宏可以插件

Code that generates codescala

還記得上篇文章中, 咱們提到的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 = scala.reflect.runtime.JavaUniverse@6d657803

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

相關文章
相關標籤/搜索