本文的行文風格不求閱讀意義上的可讀性,而是指望讀者可以跟着本文的一些探索,本身作一些嘗試,即git clone本文涉及的代碼閱讀並實踐。git
至於Scala元編程的一些介紹,請閱讀 @王在祥 的《神奇的Scala Macro之旅系列》: 一, 二, 三, 四。github
咱們從Macro Paradise的例子開始。有點遺憾的是,這個例子仍然在使用舊的Sbt。因此,咱們的第一步是把構建的定義升級到當前Sbt的最新版。完整的項目見我fork的sbt-example-paradise。編程
首先,在project/build.properties中指定:bash
sbt.version=1.2.7
複製代碼
而後,再修改build.sbt爲:網絡
val paradiseVersion = "2.1.0"
lazy val commonSettings = Seq(
scalaVersion := "2.12.8",
addCompilerPlugin("org.scalamacros" % "paradise" % paradiseVersion cross CrossVersion.full)
)
lazy val root = (project in file("."))
.aggregate(core, macros)
lazy val macros = (project in file("macros"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
)
lazy val core = (project in file("core"))
.settings(commonSettings)
.dependsOn(macros)
複製代碼
咱們能夠對比一下這一段SBT項目構建的定義和Maven的構建定義:工具
最後,運行一下這個例子:單元測試
$ sbt
> project core # 切換到core子項目
> compile
> run
複製代碼
輸出結果以下:學習
hello
複製代碼
@hello
object Test extends App {
println(this.hello)
}
複製代碼
咱們實際上運行的這段代碼異常簡潔。this指代Test這個獨立對象(stand-alone object)自己,調用了一個不存在的方法hello。咱們的問題是,@hello施放了什麼樣的魔法,生成了這樣一個不存在的hello方法。測試
忽略別的語法細節,咱們只看下面的這段代碼:ui
annottees.map(_.tree).toList match {
case q"object $name extends ..$parents { ..$body }" :: Nil =>
q"""
object $name extends ..$parents {
def hello: ${typeOf[String]} = "hello"
..$body
}
"""
}
複製代碼
從直觀的感覺,咱們能猜測到,$name
即Test,$parents
即App,$body
就是代碼的主體。parents和body前面有兩個點,區別於name。
經過$name
、$parents
、$body
這種特殊的語法形式,咱們實際上把:
object Test extends App {
println(this.hello)
}
複製代碼
變換成了:
object Test extends App {
println(this.hello)
def hello: String = "hello"
}
複製代碼
儘管咱們或許不知道其中的語法所對應的語義,更不清楚具體的實現機制,但這部分代碼的可讀性是很是棒(intuitive)的。
如今大概知道了@hello
所施放的黑魔法。下一步,咱們就得弄明白這個簡單的例子中,每一行代碼的含義。
不然,任何拙劣的模仿和嘗試,都是在浪費時間。
那咱們應該如何學習這些黑魔法呢?官網的文檔可讀性並很差,並且很多是過期的。網絡上也沒有特別友好的面向新人的教程。
追本溯源,前面的項目實際上涉及到兩個子項目,scala-reflect和paradise。在scala的源代碼中,scala-reflect相關的代碼單元測試並很少,因此咱們從paradise的單元測試開始閱讀。
git clone git@github.com:scalamacros/paradise.git
複製代碼
能夠將sbt的版本統一到1.2.7。這樣作,主要爲了防止去下載另一個Sbt的版本,浪費大量時間。
很幸運,更改版本以後,項目能夠正常編譯,測試。
$ sbt
> compile
> project tests
> test
複製代碼
這個sbt的終端保持開啓,而後用Intelli Idea打開整個項目,這樣,應該可以更快地打開整個項目,咱們在Sbt的會話中能夠看到無故跳出來的日誌:
[info] new client connected: network-1
複製代碼
大體瀏覽一下這些單元測試的代碼,能夠得到一些初步的印象。
另外,這個paradise插件將在Scala 2.13.x中內置,因此咱們還須要看一下Scala 2.13.x分支的代碼。經過git grep paradise
,能夠看到一些蛛絲馬跡。paradise的源代碼主要被引入到了compiler和reflect下面,而單元測試則是在tests/macro-annot下面。
此時,咱們能夠將前面的sbt-example-paradise升級到Scala 2.13.x:
val paradiseVersion = "2.1.0"
lazy val commonSettings = Seq(
scalaVersion := "2.13.0-M5",
scalacOptions ++= Seq("-Ymacro-annotations")
)
lazy val root = (project in file("."))
.aggregate(core, macros)
lazy val macros = (project in file("macros"))
.settings(commonSettings)
.settings(
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
)
lazy val core = (project in file("core"))
.settings(commonSettings)
.dependsOn(macros)
複製代碼
爲了不構建定義太複雜,咱們直接新開一個分支。
這裏請注意一下編譯選項scalacOptions ++= Seq("-Ymacro-annotations")
。我是在Scala源代碼中經過git grep paradise
瞥見的這個編譯選項,而後簡單看了一下相關代碼,瞭解到了其中的做用。這邊第二次說起git grep
,是由於在平常工做中,發現一些小夥伴不知道有git grep
這麼好用的工具,以爲十分詫異。
不過細想也很正常,不少時候,咱們本身所認爲的Common Sense,別人極有可能根本不瞭解。
因此,咱們直接研究最新的2.13.x,不須要任何依賴,就能夠探索Scala的元編程。
本文從一個Macro Paradise項目的示例項目,從構建和代碼閱讀的細節入手,從大致上去感知Macro Paradise的某個具體的應用場景。
原文連接:《Scala元編程:伊甸園初窺》