Scala微服務架構 二
三. Scala的Macro(宏)
Scala Macros對scala函數庫編程人員來說是一項不可或缺的編程工具,可以通過它來解決一些用普通編程或者類層次編程(type level programming)都無法解決的問題,這是因為Scala Macros可以直接對程序進行修改。
說到對程序進行修改,幾個概念一定要先理解,"編譯期"和"運行期",Java也有一個可以修改程序的功能,大家一定用過,就是反射.不是在運行期,編譯器也不知道接下來會發生什麽,會執行哪些代碼(這就是==動態性==).
而scala是java的衍生語言,自然也有反射,而且它還有一種更高級的反射,就是編譯時反射,它就是宏.
3.1 什麽是宏?
一般說來,宏是一種規則或模式,或稱語法替換 ,用於說明某一特定輸入(通常是字符串)如何根據預定義的規則轉換成對應的輸出(通常是字符串,或者是類,方法等)。這種替換在預編譯時進行,稱作宏展開。
通過上面的定義,感覺和C的宏概念差不多.但C的宏只不過是一段語法的替換,然而Scala的宏卻可以通過表達式樹控制一節代碼(類,或者方法)的生成。獲得了控制代碼的執行順序(見惰性計算和非限制函數)的能力,使得新創建的語法結構與語言內建的語法結構不可區分。
宏,從程序抽象的角度來看,可能不太容易調試和維護,但是能夠很強大的固定我們的設計. 同時使用宏能夠==大量==的減少樣板代碼.比如Scala的assert
require
就是使用宏實現的.
3.2 宏出現的意義?
- 編譯期元編程
- 更完善的錯誤檢查
3.2.1 編譯期元編程
什麽是元編程?
百度詞條的一句話:
元編程(Metaprogramming)是指某類計算機程序的編寫,這類計算機程序編寫或者操縱其他程序(或者自身)作為它們的數據,==或者在運行時完成部分本應在編譯時完成的工作==。很多情況下與手工編寫全部代碼相比工作效率更高。編寫元程序的語言稱之為元語言,被操作的語言稱之為目標語言。==一門語言同時也是自身的元語言的能力稱之為反射==。
元編程是用來產生代碼的程序,操縱代碼的程序,在運行時創建和修改代碼而非編程時,這種程序叫做元程序。而編寫這種程序就叫做元編程。
所以,元編程技術在多種編程語言中都可以使用,但更多的還是被應用於動態語言中,因為動態語言提供了更多的在運行時將代碼視為數據進行操縱的能力。
雖然靜態語言也支持元編程(反射機制),但是仍然沒有諸如Ruby這樣的更趨動態性的語言那麽透明,這是因為靜態語言在運行時其代碼和數據是分布在兩個層次上的。
3.2.2 更完善的錯誤檢查
引自知乎https://www.zhihu.com/question/27685977/answer/38014170
首先思考一個問題:如果你的應用程序有bug,那麽你希望在什麽情況下發現呢?
- 編譯時:這是最理想的狀態,如果一個bug可以通過編譯器檢查出來,那麽程序員可以在第一時間發現問題,基本上就是一邊寫一邊fix。這也正是靜態編譯型語言的強大優勢。
- 單元測試:沒有那麽理想但是也不差。每寫完一段code跑一下測試,看看有沒有新的bug出來。對於scala來說,現在的工具鏈已經不錯了,左屏sbt > ~test,右屏寫代碼愜意得很。
- 運行時:這個就比較糟糕了。運行時才報錯意味著你得首先打包部署,這個時間開銷通常就比較大,而且在許多公司,你還要時不時的解決環境問題,很是讓人抓狂。
而Scala的宏,就是可以將一些運行期才會出現的錯誤,在編譯器暴露出來.
3.3 如何使用宏?
Scala是如何使用宏的呢?
引用自https://www.cnblogs.com/tiger-xc/p/6112143.html
graph TD
A[編譯器對程序進行類型驗證] --> B{是否發現Macro標記}
B --> |否| END[進行類型驗證]
B --> |是| C[使用Macro預編譯解析程序]
C --> D[生成語法樹結構,AST,Abstract Syntax Tree]
D --> E[使用AST替代需要類型驗證的表達式]
E --> END
明白了上面的流程之後,我們出個栗子:
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)
}
}
}
以上代碼的執行邏輯如下:
graph TD
A[當編譯器在編譯modules遇到方法調用greeting時,會進行函數符號解析] --> B[調用mmacros裏的greeting方法]
B --> C{greeting方法是否為macro}
C --> |否| END[函數調用]
C --> |是| D[使用macro解析greetingMacro方法為AST]
D --> E[將生成的AST替換到表達式greeting的調用處]
E --> END
註意編譯器在運算greetingMacro時會以AST方式將參數person傳入。由於在編譯modules對象時需要運算greetingMacro函數,所以greetingMacro函數乃至整個mmacros對象必須是已編譯狀態,這就意味著modules和mmacros必須分別在不同的源代碼文件裏,而且還要確保在編譯modules前先完成對mmacros的編譯.
※. 本期語法糖
※.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
}
可以看到,和上面的IntShowable
有大量的樣板代碼.
※.2 Macro Annotations ==> @compileTimeOnly("")
官方文檔
開局處個栗子
import scala.reflect.macros.Context
import scala.language.experimental.macros
import scala.annotation.StaticAnnotation
import scala.annotation.compileTimeOnly
@compileTimeOnly("enable macro paradise to expand macro annotations")
class identity extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro ???
}
官網栗子,我們的代碼也比較常見,繼承了StaticAnnotation
,表示這是一個註解類,不懂的可以看看上一期文章.
主要說的是上面
@compileTimeOnly("enable macro paradise to expand macro annotations")
首先,這不是強制性的,即便不寫,也會被編譯器自動擴展上.但還是建議加上避免混亂.
Scala微服務架構 二