了解Scala 宏


前情回顧

了解Scala反射介紹了反射的基本概念以及運行時反射的用法, 同時簡單的介紹了一下編譯原理知識, 其中我感覺最為的地方, 就屬泛型的幾種使用方式了.

而最抽象的概念, 就是對於符號和抽象樹的這兩個概念的理解.

現在回顧一下泛型的幾種進階用法:

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

現在想想, 既然已經有了泛型了, 還要這幾個功能干嘛呢? 其實可以類比一下, 之前沒有泛型, 而為什么引入泛型呢?

當然是為了代碼更好的服用. 想象一下, 本來一個方法沒有入參, 但通過參數, 可以減少很多相似代碼.

同理, 泛型是什么, generics. 又叫什么, 類型參數化. 本來方法的入參只能接受一種類型的參數, 加入泛型后, 可以處理多種類型的入參.

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

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

Scala Macros對scala函數庫編程人員來說是一項不可或缺的編程工具,可以通過它來解決一些用普通編程或者類層次編程(type level programming)都無法解決的問題,這是因為Scala Macros可以直接對程序進行修改。

說到對程序進行修改,幾個概念一定要先理解,"編譯期"和"運行期",Java也有一個可以修改程序的功能,大家一定用過,就是反射.不是在運行期,編譯器也不知道接下來會發生什么,會執行哪些代碼(這就是==動態性==).

而scala是java的衍生語言,自然也有反射,而且它還有一種更高級的反射,就是編譯時反射,它就是宏.

什么是宏

一般說來,宏是一種規則或模式,或稱語法替換 ,用於說明某一特定輸入(通常是字符串)如何根據預定義的規則轉換成對應的輸出(通常是字符串,或者是類,方法等)。這種替換在預編譯時進行,稱作宏展開。

通過上面的定義,感覺和C的宏概念差不多.但C的宏只不過是一段語法的替換,然而Scala的宏卻可以通過表達式樹控制一節代碼(類,或者方法)的生成。獲得了控制代碼的執行順序(見惰性計算和非限制函數)的能力,使得新創建的語法結構與語言內建的語法結構不可區分。

宏,從程序抽象的角度來看,可能不太容易調試和維護,但是能夠很強大的固定我們的設計. 同時使用宏能夠==大量==的減少樣板代碼.比如Scala的assertrequire就是使用宏實現的.

宏出現的意義

  • 編譯期元編程
  • 更完善的錯誤檢查

編譯期元編程

什么是元編程?

百度詞條的一句話:

元編程(Metaprogramming)是指某類計算機程序的編寫,這類計算機程序編寫或者操縱其他程序(或者自身)作為它們的數據,==或者在運行時完成部分本應在編譯時完成的工作==。很多情況下與手工編寫全部代碼相比工作效率更高。編寫元程序的語言稱之為元語言,被操作的語言稱之為目標語言。==一門語言同時也是自身的元語言的能力稱之為反射==。

元編程是用來產生代碼的程序,操縱代碼的程序,在運行時創建和修改代碼而非編程時,這種程序叫做元程序。而編寫這種程序就叫做元編程。

所以,元編程技術在多種編程語言中都可以使用,但更多的還是被應用於動態語言中,因為動態語言提供了更多的在運行時將代碼視為數據進行操縱的能力。
雖然靜態語言也支持元編程(反射機制),但是仍然沒有諸如Ruby這樣的更趨動態性的語言那么透明,這是因為靜態語言在運行時其代碼和數據是分布在兩個層次上的。

最后可以理解為,元編程就是程序可以操作更小的粒度和動作.

更完善的錯誤檢查

引自知乎https://www.zhihu.com/question/27685977/answer/38014170

首先思考一個問題:如果你的應用程序有bug,那么你希望在什么情況下發現呢?

  • 編譯時:這是最理想的狀態,如果一個bug可以通過編譯器檢查出來,那么程序員可以在第一時間發現問題,基本上就是一邊寫一邊fix。這也正是靜態編譯型語言的強大優勢。
  • 單元測試:沒有那么理想但是也不差。每寫完一段code跑一下測試,看看有沒有新的bug出來。對於scala來說,現在的工具鏈已經不錯了,左屏sbt > ~test,右屏寫代碼愜意得很。
  • 運行時:這個就比較糟糕了。運行時才報錯意味着你得首先打包部署,這個時間開銷通常就比較大,而且在許多公司,你還要時不時的解決環境問題,很是讓人抓狂。

而Scala的宏,就是可以將一些運行期才會出現的錯誤,在編譯器暴露出來.

編譯時反射

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

Macros 能做什么呢?

直白一點, 宏能夠

Code that generates code

還記得上篇文章中, 我們提到的AST(abstract syntax tree, 抽象語法樹)嗎? Macros 可以利用 compiler plugin 在 compile-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]

記得上一期已經說過的TypeTag 和 ClassTag.

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 = { ??? } }

看着眼熟不? 沒錯, 和Scala 的List[+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之前, 我們先了解一下什么是NamesNames在官網解釋是:

Names are simple wrappers for strings.

只是一個簡單的字符串包裝器, 也就是把字符串包裝起來, Names有兩個子類, 分別是TermName 和 TypeName, 將一個字符串用兩個子類包裝起來, 就可以使用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

 

宏的編譯過程?

Scala是如何編譯宏的呢?

引用自https://www.cnblogs.com/tiger-xc/p/6112143.html
image

明白了上面的流程之后,我們出個栗子:

object modules {
    greeting("john") } object mmacros { def greeting(person: String): Unit = macro greetingMacro def greetingMacro(c: Context)(person: c.Expr[String]): c.Expr[Unit] = { import c.universe._ println("compiling greeting ...") val now = reify {new Date().toString} reify { println("Hello " + person.splice + ", the time is: " + new Date().toString) } } }

以上代碼的執行邏輯如下:
image

注意編譯器在運算greetingMacro時會以AST方式將參數person傳入。由於在編譯modules對象時需要運算greetingMacro函數,所以greetingMacro函數乃至整個mmacros對象必須是已編譯狀態,這就意味着modules和mmacros必須分別在不同的源代碼文件里,而且還要確保在編譯modules前先完成對mmacros的編譯.

編寫宏實現

其實宏的使編寫並不難,api已經幫我們做好了一切,我們只要關注如何使用獲取宏參數和宏的返回值即可.
上面栗子中的代碼,greetingMacro方法就是一個最簡單的宏實現,代碼如下:

def greetingMacro(c: Context)(person: c.Expr[String]): c.Expr[Unit] = { import c.universe._ println("compiling greeting ...") val now = reify {new Date().toString} reify { println("Hello " + person.splice + ", the time is: " + new Date().toString) } }

但是想要實現更多的功能,還需要更加深入的學習Scala的宏和表達式樹.

宏語法糖

1.implicit macor (隱式宏)

官方文檔

開局出個栗子

trait Showable[T] { 
 def show(x: T): String } def show[T](x: T)(implicit s: Showable[T]) = s.show(x) implicit object IntShowable extends Showable[Int] {  def show(x: Int) = x.toString } show(42) // return "42" show("42") // compilation error

可以調用成功show(),主要因為名稱空間存在Showable的子類IntShowable,並且是implicit object,這個implicit object的作用上一篇已經講過了,就不說了.

上面代碼,乍一看還可以,但是如果擴展起來就不是很舒服了,如果要讓show("42")也可用,我們就需要添加如下代碼:

implicit object StringShowable extends Showable[String] { def show(x: String) = x }

2.宏注解 Macro Annotations ==> @compileTimeOnly("")

官方文檔

開局處個栗子,可以自動為case classclass在編譯時生成一個名為TempLog的方法.

import scala.reflect.macros.Context
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.annotation.compileTimeOnly

@compileTimeOnly("temp log print") class AnPrint(msg: Any*) extends StaticAnnotation { def macroTransform(annottees : Any*) : Any = macro AnPrintMacroImpl.impl }

官網栗子,我們的代碼也比較常見,繼承了StaticAnnotation,表示這是一個注解類,有興趣的朋友可以看看上一期文章.

主要說的是上面

@compileTimeOnly("temp log print")

官網解釋

First of all, note the @compileTimeOnly annotation. It is not mandatory, but is recommended to avoid confusion

首先,這不是強制性的,即便不寫,也會被編譯器自動擴展上.但還是建議加上避免混亂.

然后是宏的具體實現,如下:

object AnPrintMacroImpl {
    def impl(c : whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { import c.universe._ val tree = annottees.map(_.tree).toList.head val (className, fields, parents, body) = tree match{ case q"case class $className(..$fields) extends ..$parents { ..$body }" => (className, fields, parents, body) case q"class $className(..$fields) extends ..$parents { ..$body }" => (className, fields, parents, body) } //TempLog val LogDefName = TermName("TempLog") val LogDefImpl = q"""def $LogDefName(sss: Any):Unit=println(" ===> " + sss)""" val out = q""" case class $className(..$fields) extends ..$parents { ..$LogDefImpl ..$body } """ println(showRaw(tree)) c.Expr(out) } }

里面的具體細節,主要是宏將類變成AST,然后利用模式匹配,來解析類信息,之后可以加入自己定義的任何操作,最后用Block封裝起來,這樣子,一個簡單的宏就實現了.

我們測試一下:

package myTest

@AnPrint("clock") class ccc(val a: String = "aaa", val b: String = "bbb"){ TempLog("init b") } object annotationPrintTest extends App { println("start") val a = new b("aiyou", "wolegequ") a.TempLog("打印我了") println("end") }

注意,這里需要先編譯AnPrintMacroImplAnPrint文件,才可以測試通過

打印結果如下:

start
===> init b ===> 打印我了 end


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM