Scala元編程:伊甸園初窺

閱讀建議

本文的行文風格不求閱讀意義上的可讀性,而是指望讀者可以跟着本文的一些探索,本身作一些嘗試,即git clone本文涉及的代碼閱讀並實踐。git

至於Scala元編程的一些介紹,請閱讀 @王在祥 的《神奇的Scala Macro之旅系列》: , , , github

繞不開的Sbt

咱們從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的構建定義:工具

  1. commonSettings至關於Maven裏面最頂層的pom.xml
  2. root至關於Maven中指定子模塊的那幾行
  3. macros和core分別對應兩個子目錄,其中,core依賴macros。

最後,運行一下這個例子:單元測試

$ 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
複製代碼

大體瀏覽一下這些單元測試的代碼,能夠得到一些初步的印象。

Macro Paradise將在Scala 2.13.x中內置

另外,這個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元編程:伊甸園初窺》

相關文章
相關標籤/搜索