瞭解Scala反射介紹了反射的基本概念以及運行時反射的用法, 同時簡單的介紹了一下編譯原理知識, 其中我感受最爲繞
的地方, 就屬泛型的幾種使用方式了.javascript
而最抽象的概念, 就是對於符號和抽象樹的這兩個概念的理解.html
如今回顧一下泛型的幾種進階用法:java
如今想一想, 既然已經有了泛型了, 還要這幾個功能幹嗎呢? 其實能夠類比一下, 以前沒有泛型, 而爲何引入泛型呢?python
固然是爲了代碼更好的服用. 想象一下, 原本一個方法沒有入參, 但經過參數, 能夠減小不少類似代碼.程序員
同理, 泛型是什麼, generics. 又叫什麼, 類型參數化. 原本方法的入參只能接受一種類型的參數, 加入泛型後, 能夠處理多種類型的入參.sql
順着這條線接着往下想, 有了逆變和協變, 咱們讓泛型的包裝類也有了類繼承關係, 有了繼承的層級關係, 方法的處理能力又會大大增長.編程
泛型, 並不神奇, 只是省略了一系列代碼, 並且引入泛型還會致使泛型擦除
, 以及一系列的隱患. 而類型擦除其實也是爲了兼容更早的語言, 咱們一籌莫展.
但泛型在設計上實現的數據和邏輯分離, 卻能夠大大提升程序代碼的簡潔性和可讀性, 並提供可能的編譯時類型轉換安全檢測功能. 因此在可使用泛型的地方咱們仍是推薦的.api
Scala Macros對scala函數庫編程人員來講是一項不可或缺的編程工具,能夠經過它來解決一些用普通編程或者類層次編程(type level programming)都沒法解決的問題,這是由於Scala Macros能夠直接對程序進行修改。安全
說到對程序進行修改,幾個概念必定要先理解,"編譯期"和"運行期",Java也有一個能夠修改程序的功能,你們必定用過,就是反射.不是在運行期,編譯器也不知道接下來會發生什麼,會執行哪些代碼(這就是==動態性==).ruby
而scala是java的衍生語言,天然也有反射,並且它還有一種更高級的反射,就是編譯時反射,它就是宏.
通常說來,宏是一種規則或模式,或稱語法替換 ,用於說明某一特定輸入(一般是字符串)如何根據預約義的規則轉換成對應的輸出(一般是字符串,或者是類,方法等)。這種替換在預編譯時進行,稱做宏展開。
經過上面的定義,感受和C的宏概念差很少.但C的宏只不過是一段語法的替換,然而Scala的宏卻能夠經過表達式樹控制一節代碼(類,或者方法)的生成。得到了控制代碼的執行順序(見惰性計算和非限制函數)的能力,使得新建立的語法結構與語言內建的語法結構不可區分。
宏,從程序抽象的角度來看,可能不太容易調試和維護,可是可以很強大的固定咱們的設計. 同時使用宏可以==大量==的減小樣板代碼.好比Scala的assert
和require
就是使用宏實現的.
什麼是元編程?
百度詞條的一句話:
元編程(Metaprogramming)是指某類計算機程序的編寫,這類計算機程序編寫或者操縱其餘程序(或者自身)做爲它們的數據,==或者在運行時完成部分本應在編譯時完成的工做==。不少狀況下與手工編寫所有代碼相比工做效率更高。編寫元程序的語言稱之爲元語言,被操做的語言稱之爲目標語言。==一門語言同時也是自身的元語言的能力稱之爲反射==。
元編程是用來產生代碼的程序,操縱代碼的程序,在運行時建立和修改代碼而非編程時,這種程序叫作元程序。而編寫這種程序就叫作元編程。
因此,元編程技術在多種編程語言中均可以使用,但更多的仍是被應用於動態語言中,由於動態語言提供了更多的在運行時將代碼視爲數據進行操縱的能力。
雖然靜態語言也支持元編程(反射機制),可是仍然沒有諸如Ruby這樣的更趨動態性的語言那麼透明,這是由於靜態語言在運行時其代碼和數據是分佈在兩個層次上的。
最後能夠理解爲,元編程就是程序能夠操做更小的粒度和動做.
引自知乎https://www.zhihu.com/question/27685977/answer/38014170
首先思考一個問題:若是你的應用程序有bug,那麼你但願在什麼狀況下發現呢?
而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
2.11 +
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, 不過, 請不要變的太離譜.
方法宏, 其實以前的代碼中, 已經見識過了, 沒什麼稀奇, 但剛纔的例子仍是比較簡單的, 若是咱們要傳遞一個參數, 或者泛型呢?
看下面例子:
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
實現也多了兩個括號
咱們來一一講解
這是Macros
的表達式包裝器, 裏面放置着類型String
, 爲何不能直接傳String
呢?
固然是不能夠了, 由於宏的入參只接受Expr, 調用宏傳入的參數也會默認轉爲Expr.
這裏要注意, 這個
(s: c.Expr[String])
的入參名必須等於hello2[T](s: String)
的入參名
記得上一期已經說過的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()
, 這是作什麼的呢?
咱們能夠理解爲這是一個AST構建函數, 比較好奇的我看了下源碼, 搜打死乃.
class ApplyExtractor{ def apply(fun: Tree, args: List[Tree]): Apply = { ??? } }
看着眼熟不? 沒錯, 和Scala
的List[+A]
的構建函數相似, 一個延遲建立函數. 好了, 先理解到這.
定義, 能夠理解爲Scala標識符的構建函數.
文字, 字符串構建函數
選擇構建函數, 選擇的什麼呢? 答案是一切, 不管是選擇方法, 仍是選擇類. 咱們能夠理解爲.
這個調用符. 舉個例子吧:
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
以前, 咱們先了解一下什麼是Names
, Names
在官網解釋是:
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""" ??? """
同樣, 可使用$
引用外部屬性, 方便進行邏輯處理.
宏註釋, 就和咱們在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) } }
非強制的, 但建議加上. 官網解釋以下:
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
的類, 將被Scala解釋器標記爲註解類, 以註解的方式使用, 因此不建議直接生成實例, 加上final
修飾符.
def macroTransform(annottees: Any*): Any = macro ToStringMacro.impl
對於使用@ToStringMacro
修飾的代碼, 編譯器會自動調用macroTransform
方法, 該方法的入參, 是annottees: Any*
, 返回值是Any
, 主要是由於Scala
缺乏更細緻的描述, 因此使用這種籠統的方式描述能夠接受一切類型參數.
而方法的實現, 和Def Macro
同樣.
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 !") } ??? } }
有幾點須要注意的地方:
Scala 推出了一款插件, 叫作Macro Paradise(宏天堂), 能夠幫助開發者控制帶有宏的Scala代碼編譯順序, 同時還提供調試功能, 這裏不作過多介紹, 有興趣的能夠查看官網: Macro Paradise
Scala是如何編譯宏的呢?
引用自http://www.javashuo.com/article/p-mnkbrolf-my.html
明白了上面的流程以後,咱們出個栗子:
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) } } }
以上代碼的執行邏輯以下:
注意編譯器在運算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的宏和表達式樹.
開局出個栗子
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 }
開局處個栗子,能夠自動爲case class
或class
在編譯時生成一個名爲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") }
注意,這裏須要先編譯
AnPrintMacroImpl
和AnPrint
文件,才能夠測試經過
打印結果以下:
start
===> init b ===> 打印我了 end