1. 程式人生 > >Scala超程式設計:伊甸園初窺

Scala超程式設計:伊甸園初窺

閱讀建議

本文的行文風格不求閱讀意義上的可讀性,而是期望讀者能夠跟著本文的一些探索,自己做一些嘗試,即git clone本文涉及的程式碼閱讀並實踐。

至於Scala超程式設計的一些介紹,請閱讀 @王在祥 的《神奇的Scala Macro之旅系列》: , , ,

繞不開的Sbt

我們從Macro Paradise的例子開始。有點遺憾的是,這個例子仍然在使用舊的Sbt。所以,我們的第一步是把構建的定義升級到當前Sbt的最新版。完整的專案見我fork的sbt-example-paradise

首先,在project/build.properties中指定:

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方法。

忽略別的語法細節,我們只看下面的這段程式碼:

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 [email protected]: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的某個具體的應用場景。