準引號

衛生

語言
此文件頁面特別針對 Scala 2 中發布的功能,這些功能已在 Scala 3 中移除或由其他功能取代。除非另有說明,此頁面中的所有程式碼範例都假設您使用的是 Scala 2。

Denys Shabalin、Eugene Burmako 實驗性

衛生觀念已廣泛普及於 Scheme 中巨觀研究。如果一個程式碼產生器確保常規程式碼與產生程式碼之間沒有名稱衝突,防止意外擷取識別碼,則稱之為衛生。正如許多經驗報告所顯示,衛生對於程式碼產生非常重要,因為名稱繫結問題通常不顯而易見,而缺乏衛生可能會以微妙的方式顯現。

例如 Racket 等精密巨集系統具備機制,讓巨集撰寫者無需費力即可讓巨集具有衛生性。在 Scala 中,我們沒有自動衛生機制 - 我們的兩個程式碼產生設施(使用巨集的編譯時間程式碼產生,以及使用工具箱的執行時間程式碼產生)都需要程式設計師手動處理衛生。您必須知道如何解決衛生問題,而這就是本節的重點。

防止常規程式碼與產生程式碼之間出現名稱衝突意味著兩件事。首先,我們必須確保,無論我們將產生程式碼置於何種情境中,其意義都不會改變(參照透明性)。其次,我們必須確保,無論我們將常規程式碼拼接於何種情境中,其意義都不會改變(通常稱為狹義衛生)。讓我們看看在一些範例中,我們可以採取哪些措施來達到這個目的。

參照透明性

參照透明性意味著準引號應記住其定義中的詞彙情境。例如,如果在準引號的定義位置有提供匯入,則應使用這些匯入來解析準引號中的名稱。遺憾的是,目前並非如此,以下是一個範例

scala> import collection.mutable.Map

scala> def typecheckType(tree: Tree): Type =
         toolbox.typecheck(tree, toolbox.TYPEmode).tpe

scala> typecheckType(tq"Map[_, _]") =:= typeOf[Map[_, _]]
false

scala> typecheckType(tq"Map[_, _]") =:= typeOf[collection.immutable.Map[_, _]]
true

在此,我們可以看到對 Map 的非限定參照並未尊重我們的自訂匯入,而是解析為預設的 collection.immutable.Map。如果巨集中未完全限定參照,可能會出現類似的問題。

// ---- MyMacro.scala ----
package example

import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros

object MyMacro {
  def wrapper(x: Int) = { println(s"wrapped x = $x"); x }
  def apply(x: Int): Int = macro impl
  def impl(c: Context)(x: c.Tree) = {
    import c.universe._
    q"wrapper($x)"
  }
}

// ---- Test.scala ----
package example

object Test extends App {
  def wrapper(x: Int) = x
  MyMacro(2)
}

如果我們同時編譯巨集及其用法,我們將會看到當應用程式執行時,println 將不會被呼叫。這會發生是因為在巨集擴充後,Test.scala 將會看起來像

// Expanded Test.scala
package example

object Test extends App {
  def wrapper(x: Int) = x
  wrapper(2)
}

wrapper 將會解析為 example.Test.wrapper 而不是預期的 example.MyMacro.wrapper。為了避免參考透明度陷阱,可以使用兩種可能的解決方案

  1. 完全限定所有參考。也就是說,我們可以將巨集的實作調整為

    def impl(c: Context)(x: c.Tree) = {
      import c.universe._
      q"_root_.example.MyMacro.wrapper($x)"
    }
    

    _root_ 開始很重要,否則如果在巨集的使用位置重新定義 example,仍然會有名稱衝突的機會。

  2. 取消引用符號,而不是使用純識別碼。也就是說,我們可以手動解析對 wrapper 的參考

    def impl(c: Context)(x: c.Tree) = {
      import c.universe._
      val myMacro = symbolOf[MyMacro.type].asClass.module
      val wrapper = myMacro.info.member(TermName("wrapper"))
      q"$wrapper($x)"
    }
    

狹義的衛生

「狹義的衛生」是指準引號不應與取消引用到其中的樹的繫結混淆。例如,如果取消引用到巨集擴充的巨集引數最初是指封閉詞彙脈絡中的某個變數,那麼這個參考在巨集擴充後應該仍然有效,無論為該巨集擴充產生什麼程式碼。不幸的是,我們沒有自動設施來確保這一點,這可能會導致意外情況

scala> val originalTree = q"val x = 1; x"
originalTree: universe.Tree = ...

scala> toolbox.eval(originalTree)
res1: Any = 1

scala> val q"$originalDefn; $originalRef" = originalTree
originalDefn: universe.Tree = val x = 1
originalRef: universe.Tree = x

scala> val generatedTree = q"$originalDefn; { val x = 2; println(x); $originalRef }"
generatedTree: universe.Tree = ...

scala> toolbox.eval(generatedTree)
2
res2: Any = 2

在那個範例中,val x = 2 的定義遮蔽了原始樹中建立的 xval x = 1 的繫結,改變了產生程式碼中 originalRef 的語意。在這個簡單的範例中,遮蔽很容易遵循,然而在精細的巨集中,它很容易失控。

為避免這些情況,有一個從 Lisp 早期開始就經過實戰考驗的解決方法;建立一個函數,用於在產生的程式碼中建立唯一的名稱。在 Lisp 語言中,它稱為 gensym,而在 Scala 中,我們稱它為 freshName。準引號在此特別有用,因為它們允許將產生的名稱直接取消引號,並放入產生的程式碼中。

不過,我們的 API 有點混亂。有一個內部 API internal.reificationSupport.{ freshTermName, freshTypeName } 可用於編譯時間和執行時間的宇宙,不過只有在編譯時間才有對應的良好公開外觀,稱為 c.freshName。我們計畫在 Scala 2.12 中修正這個問題。

scala> val xfresh = universe.internal.reificationSupport.freshTermName("x$")
xfresh: universe.TermName = x$1

scala> val generatedTree = q"$originalDefn; { val $xfresh = 2; println($xfresh); $originalRef }"
generatedTree: universe.Tree = ...

scala> toolbox.eval(generatedTree)
2
res2: Any = 1

此頁面的貢獻者