巨集

Scala 2.11 中的變更

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

實驗性

Eugene Burmako

本文件列出 Scala 2.11.0 開發週期中反射和巨集的所有重大變更。首先,我們提供最重要的修正和新引入功能的摘要,然後在本文檔的後續部分說明這些變更如何影響與 Scala 2.10.x 的相容性,以及如何讓基於反射的程式碼同時在 2.10.x 和 2.11.0 中執行。

準引號

準引號是 Scala 2.11.0 中反射和巨集最令人印象深刻的單一升級。由 Denys Shabalin 實作,它們大幅簡化了全球 Scala 元程式設計師的生活。請參閱 專屬文件頁面 以進一步了解準引號。

新的巨集功能

1) Fundep 實體化。自 Scala 2.10.2 以來,隱含 whitebox 巨集可用於實體化類別類別的執行個體,但此類實體化執行個體無法引導類型推論。在 Scala 2.11.0 中,實體化器也可以影響類型推論,協助 scalac 推論封閉方法應用程式的類型引數,這是 Shapeless 中廣泛成功使用的功能。此外,透過修正 SI-3346,此推論引導功能可以同時影響一般方法和隱含轉換。不過,請注意,fundep 實體化並未讓使用者變更 Scala 類型推論運作的方式,而是僅提供一種將更多類型限制納入組合的方式,因此,例如,不可能使用 fundep 實體化器讓類型推論從右到左進行。

2) 萃取巨集。Scala 2.11.0 中一個重要的新功能是 Paul Phillips 實作的 基於名稱的萃取器。而一如往常,當 Scala 有新功能時,巨集很可能會使用它。確實,在結構化類型的協助下,白色方塊巨集可以用來撰寫萃取器,以逐案方式精煉萃取對象的類型。這是我們在內部實作準引號所使用的技術。

3) 巨集中命名和預設參數。嚴格來說,這不應該屬於這個變更日誌,因為這個功能在合併到 Scala 2.11.0-RC1 後不久就因為一個導致回歸的小錯誤而被還原,但我們有一個修補程式讓巨集引擎了解巨集應用程式中的命名/預設參數。即使程式碼凍結不讓我們在 Scala 2.11.0 中加入這個變更,我們預計會在最早的時間將它合併到 Scala 2.11.1 中。

4) 類型巨集巨集註解。類型巨集和巨集註解都不包含在 Scala 2.11.0 中。類型巨集不太可能包含在 Scala 中,但我們仍在考慮巨集註解。然而,巨集註解可透過 巨集天堂外掛程式 在 Scala 2.10.x 和 Scala 2.11.0 中使用。

5) @compileTimeOnly。標準函式庫現在提供一個新的 scala.annotations.compileTimeOnly 註解,用來告訴 scalac,其註解對象不應在類型檢查後(包括巨集擴充)被參照。此註解的主要用途是標記只應與封閉巨集呼叫一起使用的輔助方法,以指出該巨集呼叫中需要特殊處理的部分引數(例如 scala/async 中的 await 或 sbt 的新巨集基礎 DSL 中的 value)。例如,scala/async 的 await 標記為 @compileTimeOnly,僅在 async { ... } 區塊中才有意義,該區塊會在轉換期間將其編譯,並使用 async 外部,由於新的註解,會產生編譯時期錯誤。

巨集引擎的變更

6) 黑盒/白盒分離。巨集實作使用 scala.reflect.macros.blackbox.Context(Scala 2.11.0 中的新增功能)的巨集稱為黑盒,與 2.10.x 中的巨集相比,功能較少,在 IDE 中有更好的支援,且更有可能成為 Scala 的一部分。巨集實作使用 scala.reflect.macros.whitebox.Context(Scala 2.11.0 中的新增功能)或 scala.reflect.macros.Context(Scala 2.10.x 中唯一的內容,已在 Scala 2.11.0 中棄用)的巨集稱為白盒,且至少具有與 2.10.x 中的巨集相同的功能。

7) 巨集套件。眾所周知,當前反射 API 的路徑依賴性(在 Scala 2.10.x 和 Scala 2.11.0 中都有)使得巨集模組化變得困難。有 設計模式 可以幫助克服這個困難,但這只會導致樣板程式碼的激增。處理這個問題的方法之一是完全取消蛋糕,而這就是我們在 Project Palladium 中追求的,但這對於 Scala 2.11.0 來說是一個太大的改變,所以我們想出了在真正的解決方案出現之前可以減輕這個問題的解決方法。巨集套件是具有單一公開欄位類型為 Context 的類別,並且套件中的任何公開方法都可以稱為巨集實作。然後,這樣的巨集實作可以輕鬆呼叫同一個類別或其超類別的其他方法,而無需攜帶上下文,因為套件已經攜帶了每個人都可以看到和引用的上下文。這顯著簡化了編寫和維護複雜巨集的過程。

8) 放寬巨集實作簽名的需求。隨著準引號的出現,reify 很快地因過於笨重和不靈活而失寵。為了認可這一點,我們現在允許巨集實作的引數和回傳類型為 c.Tree 而不是 c.Expr[Something]。不再需要撰寫巨大的類型簽名,然後花費時間和程式碼行來嘗試將巨集實作與這些類型對齊。只需輸入樹並回傳樹即可 - 樣板程式碼已經消失了。

9) 巨集定義回傳型別的推論正在逐步淘汰。考量到新架構,巨集實作可以回傳 c.Tree,而不是 c.Expr[Something],因此無法再從巨集實作的回傳型別穩健地推論巨集定義的回傳型別(如果巨集實作回傳 c.Tree,那這棵樹的型別會是什麼?)。因此,我們正在逐步淘汰此語言機制。回傳 c.Expr[T] 的巨集實作仍可拿來推論其巨集定義的回傳型別,但會產生不建議使用的警告,而嘗試使用回傳 c.Tree 的巨集實作來推論巨集定義的回傳型別,則會導致編譯錯誤。

10) 巨集擴充型別檢查方式變更。在 Scala 2.10.x 中,巨集擴充會檢查型別兩次:第一次針對對應巨集定義的回傳型別檢查(稱為 innerPt),第二次針對從封裝程式衍生的預期型別檢查(稱為 outerPt)。當回傳型別誤導型別推論,而巨集擴充最後具有不精確型別時,這會導致某些罕見的問題。在 Scala 2.11.0 中,型別檢查架構已變更。黑盒巨集仍會針對 innerPt 和 outerPt 檢查型別,但白盒巨集會先在沒有任何預期型別的情況下鍵入(即針對 WildcardType),然後才針對 innerPt 和 outerPt 鍵入。

11) 複製所有進出資料。不幸的是,反射 API 的核心資料結構(樹、符號、類型)本身可以變異,或可傳遞變異。這使得 API 變得脆弱,因為很容易在不知不覺中改變某人的狀態,而這些狀態與其未來的客戶端不相容。我們還沒有針對此問題找到完整的解決方案,但我們已對巨集引擎套用多項防護措施,以某種程度遏制變異的可能性。特別是,我們現在複製巨集實作的所有引數和傳回值,以及所有可能變異 API 的輸入和輸出,例如 Context.typeCheck

反射 API 的變更

12) 引入 Universe.internal 和 Context.internal。Scala 2.10.x 反射 API 使用者的回饋提供了兩個重要的見解。首先,我們公開的某些功能層級太低,而且在公開 API 中非常不恰當。其次,某些低層級功能對於讓重要的巨集運作非常重要。為了在某種程度上解決這兩個開發向量所造成的緊張關係,我們已建立公開 API 的內部子區段,這些子區段:a) 與反射 API 的已驗證部分明確區分,b) 提供給知道自己在做什麼並想要實作我們想要支援的實際重要使用案例的人。請參閱文件底部的遷移和相容性注意事項,以了解更多資訊。

13) 執行時期反射的執行緒安全性。Scala 2.10.x 中反射中最迫切的問題是其執行緒安全性。嘗試從多個執行緒使用執行時期反射(例如類型標籤)會導致上面記錄的奇怪崩潰。我們相信已在 Scala 2.11.0-RC1 中修復此問題,方法是在我們實作的重要位置引入多個鎖定。一方面,我們目前使用的策略在某種意義上並非最佳,因為某些經常使用的操作(例如 Symbol.typeSignatureTypeSymbol.typeParams)隱藏在全域鎖定之後,但我們計畫在未來最佳化此問題。另一方面,大多數典型的 API(例如 Type.membersType.<:<)使用執行緒區域狀態或根本不需要同步,因此絕對值得嘗試。

14) Type.typeArgs。現在取得給定類型的類型參數非常簡單。在 Scala 2.10.x 中需要模式比對的內容現在只需簡單呼叫方法即可。 typeArgs 方法也加入了 typeParamsparamListsresultType,讓執行常見類型檢查工作變得非常容易。

15) symbolOf[T]。Scala 2.11.0 針對非常常見的 typeOf[T].typeSymbol 作業引入一個捷徑,讓找出給定類別和物件的元資料(註解、旗標、可見性等)變得更容易。

16) knownDirectSubclasses 被視為正式中斷。許多嘗試遍歷類別的密封階層的使用者注意到,ClassSymbol.knownDirectSubclasses 僅在 Scala 編譯順序中,其巨集呼叫出現在這些階層定義之後時才有效。例如,如果在原始檔的底部定義了一個密封階層,而在檔案的頂部寫入一個巨集應用程式,則 knownDirectSubclasses 會傳回一個空清單。這是一個根植於 Scala 內部架構的問題,我們無法在近期內提供修復程式。

17) showCode。除了列印類似 Scala 的原始碼的 Tree.toString,以及列印樹狀結構內部結構的 showRaw(tree) 之外,我們現在有了 showCode,它會列印與提供的樹狀結構對應的可編譯 Scala 原始碼,這要歸功於 Vladimir Nikolaev,他為此付出了驚人的努力。我們計畫最終使用 showCode 取代 Tree.toString,但在 Scala 2.11.0 中,這兩個是不同的方法。

18) 現在可以在類型和模式模式中進行類型檢查。Scala 2.10.x 的 Context.typeCheckToolBox.typeCheck 功能非常方便,但有一個很大的不便 - 它僅適用於表達式,而將某些內容作為類型或模式進行類型檢查需要建立虛擬表達式。現在 typeCheck 有 mode 參數,可以處理這個困難。

19) 反射呼叫現在支援值類別。執行時期反射現在正確處理方法和建構函式參數中的值類別,並且正確地將反射呼叫的輸入和輸出解封裝和封裝,例如 FieldMirror.getFieldMirror.setMethodMirror.apply

20) 反射呼叫變得更快。在全新推出的 FieldMirror.bindMethodMirror.bind API 的幫助下,現在可以快速從現有鏡像建立新的鏡像,避免進行昂貴的鏡像初始化。在我們的測試中,呼叫密集型場景由於這些新的 API 而展現出高達 20 倍的效能提升。

21) Context.introduceTopLevel。作為邁向型別巨集的踏腳石,Context.introduceTopLevel API 過去在 Scala 2.11.0 的早期里程碑版本中可用,但已從最終版本中移除,因為型別巨集被拒絕納入 Scala 並在巨集天堂中停用。

如何在 2.11.0 中讓您的 2.10.x 巨集運作

22) 黑盒/白盒。Scala 2.10.x 中的所有巨集都是白盒,並且會相應地表現,能夠在擴充中精煉其巨集 def 的宣告回傳型別。請參閱本文檔的後續部分,以取得有關如何讓 Scala 2.10.x 中的巨集在 Scala 2.11.0 中表現得完全像黑盒巨集一樣的資訊。

23) 巨集套件。Scala 2.11.0 現在辨識巨集 def 右側巨集實作的新形狀參考,在某些非常罕見的情況下,這可能會改變現有程式碼的編譯方式。首先,這種情況不會影響任何執行時期行為 - 如果 Scala 2.10.x 巨集 def 在 Scala 2.11.0 中編譯,那麼它將繫結到與先前相同的巨集實作。其次,在某些情況下,巨集實作參考可能會變得模稜兩可並導致編譯失敗,但這應該可以透過錯誤訊息建議的簡單重新命名,以向後相容的方式修復。

24) 巨集定義回傳型別的推論。在 Scala 2.11.0 中,巨集定義的回傳型別會從關聯的巨集實作中推論,這將與 Scala 2.10.x 一致運作。此類巨集定義會發出不建議使用的警告,但不會發生編譯錯誤或行為差異。

25) 巨集擴充套件類型檢查方式的變更。Scala 2.11.0 變更了用於類型檢查白盒巨集擴充套件的預期型別順序(由於 Scala 2.10.x 中的所有巨集都是白盒,因此所有巨集都可能受到影響)。在罕見情況下,當 Scala 2.10.x 巨集擴充套件依賴預期型別的特定形狀來推論其型別引數時,它可能會停止運作。在這種情況下,明確指定這些型別引數將以與 Scala 2.10.x 和 Scala 2.11.0 相容的方式解決問題:範例

26) 進出的一切內容的重複。在 Scala 2.11.0 中,我們會一致地重複跨越使用者空間(巨集實作程式碼)和核心(編譯器內部)邊界的樹狀結構,限制這些樹狀結構的變異範圍。在極罕見的情況下,Scala 2.10.x 巨集可能依賴此類變異才能正確運作。此類巨集將會損壞,必須重新撰寫。不過不用太擔心,因為我們尚未在實際環境中遇到此類巨集,因此您的巨集很可能沒問題。

27) Universe.internal 和 Context.internal 的引入。Scala 2.10.x 中提供的以下 51 個 API 已移至反射蛋糕的 internal 子模組。有兩種方法可以解決這些來源不相容性。簡單的方法是在 import scala.reflect.runtime.universe._import c.universe._ 之後撰寫 import compat._。困難的方法是簡單的方法 + 套用從 compat 匯入的方法中不建議使用的警告提供的全部遷移建議。

     
typeTagToManifest Tree.pos_= Symbol.isSkolem
manifestToTypeTag Tree.setPos Symbol.deSkolemize
newScopeWith Tree.tpe_= Symbol.attachments
BuildApi.setTypeSignature Tree.setType Symbol.updateAttachment
BuildApi.flagsFromBits Tree.defineType Symbol.removeAttachment
BuildApi.emptyValDef Tree.symbol_= Symbol.setTypeSignature
BuildApi.This Tree.setSymbol Symbol.setAnnotations
BuildApi.Select TypeTree.setOriginal Symbol.setName
BuildApi.Ident Symbol.isFreeTerm Symbol.setPrivateWithin
BuildApi.TypeTree Symbol.asFreeTerm captureVariable
Tree.freeTerms Symbol.isFreeType referenceCapturedVariable
Tree.freeTypes Symbol.asFreeType capturedVariableType
Tree.substituteSymbols Symbol.newTermSymbol singleType
Tree.substituteTypes Symbol.newModuleAndClassSymbol refinedType
Tree.substituteThis Symbol.newMethodSymbol typeRef
Tree.attachments Symbol.newTypeSymbol intersectionType
Tree.updateAttachment Symbol.newClassSymbol polyType
Tree.removeAttachment Symbol.isErroneous existentialAbstraction

28) 已知直接子類別的官方故障。除了注意此 API 的限制之外,您無法從您的角度執行任何操作。使用 knownDirectSubclasses 的巨集將繼續在 Scala 2.11.0 中執行,就像它們在 Scala 2.10.x 中所做的那樣,而不會出現任何棄用警告。

29) Context.enclosingTree 風格 API 的棄用。現有的 enclosing tree 巨集 API 面臨技術和哲學問題,因此我們做出艱難的決定逐步淘汰它們,在 Scala 2.11.0 中棄用它們,並在 Scala 2.12.0 中移除它們。這些 API 沒有直接替換,只有新推出的 c.internal.enclosingOwner,它只涵蓋了它們功能的一部分。請追蹤 https://github.com/scala/scala/pull/3354 上的討論,以獲取更多資訊。

30) 其他棄用。你們有些人已在建置中啟用 -Xfatal-warnings,因此任何棄用都可能導致編譯失敗。本指南已涵蓋所有有爭議的棄用,其餘的棄用可以透過直接遵循棄用訊息來修正。

31) 移除 resetAllAttrs。resetAllAttrs 是非常危險的 API,一開始就不應該公開。這就是為什麼我們在沒有經過棄用週期的情況下移除它的原因。然而,有一個公開可用的替換,稱為 resetLocalAttrs,它在幾乎所有情況下都已足夠,我們建議改用它。在 resetLocalAttrs 不夠用的例外情況下,請使用 https://github.com/scalamacros/resetallattrs

32) 移除 isLocalSymbol.isLocal 沒有發揮宣傳的效果,而且無法修復。因此,我們在沒有任何棄用警告的情況下移除它,並建議改用 Symbol.isPrivateThis 和/或 Symbol.isProtectedThis

33) 移除 isOverride。與 Symbol.isLocal 的情況相同。這個方法已損壞到無法修復,這就是它從公開 API 中移除的原因。應該改用 Symbol.allOverriddenSymbols(或其新推出的別名 Symbol.overrides)。

如何讓您的 2.11.0 巨集在 2.10.x 中運作

34) 準引號。我們不打算將準引號作為 Scala 2.10.x 發行版的一部分發布,但它們仍可透過巨集天堂外掛程式在 Scala 2.10.x 中使用。閱讀 天堂文件 以瞭解使用編譯器外掛程式所需的內容、二進位相容性的後果以及支援保證為何。

35) 大部分新功能在 2.10.x 中沒有對應功能。我們不打算將任何新功能,例如 fundep 具現化或巨集套件,回溯移植到 Scala 2.10.x(除了可能執行階段反射的執行緒安全性)。請參閱 Scala 2.10.x 的巨集天堂路線圖 以查看天堂中支援哪些功能。

36) 黑盒/白盒。如果您決心讓您的巨集成為黑盒,那麼讓這些巨集在 2.10.x 和 2.11.0 中一致運作將需要額外的努力,因為在 2.10.x 中所有巨集都是白盒。首先,請確定您實際上沒有使用任何 白盒權限,否則您必須先改寫您的巨集。其次,在從巨集實作傳回之前,請將擴充明確向上轉型為其巨集定義所需的類型。(當然,這一切都不適用於白盒巨集。如果您不介意您的巨集是白盒,那麼您不必做任何事情來確保跨相容性)。

object Macros {
  def impl(c: Context) = {
    import c.universe._
    q"new { val x = 2 }"
  }

  def foo: Any = macro impl
}

object Test extends App {
  // works in Scala 2.10.x and Scala 2.11.0 if foo is whitebox
  // doesn't work in Scala 2.11.0 if foo is blackbox
  println(Macros.foo.x)
}

object Macros {
  def impl(c: Context) = {
    import c.universe._
    q"(new { val x = 2 }): Any" // note the explicit type ascription
  }

  def foo: Any = macro impl
}

object Test extends App {
  // consistently doesn't work in Scala 2.10.x and Scala 2.11.0
  // regardless of whether foo is whitebox or blackbox
  println(Macros.foo.x)
}

37) @compileTimeOnly。自 Scala 2.10.1 起,compileTimeOnly 註解在 scala-reflect.jar 中以 scala.reflect.internal.compileTimeOnly 的形式秘密提供(相比之下,在 Scala 2.11.0 中,compileTimeOnly 位於 scala-library.jar 中,名稱為 scala.annotations.compileTimeOnly)。如果您不介意 API 使用者必須傳遞依賴於 scala-reflect.jar,那麼即使在 Scala 2.10.x 中也可以繼續使用 compileTimeOnly,它將以與 Scala 2.11.0 中相同的方式運作。

38) 巨集擴充套件類型檢查方式的變更。Scala 2.11.0 變更了用於類型檢查白盒巨集擴充套件的預期類型順序(由於 Scala 2.10.x 中的所有巨集都是白盒,因此它們都可能受到影響),這在理論上可能導致類型推論問題。即使從 2.10.x 遷移到 2.11.0,這也不太可能成為問題,但反向進行幾乎沒有機會造成問題。如果您遇到困難,那麼就像任何類型推論故障一樣,請嘗試透過將巨集擴充套件向上轉型為您想要的類型來提供明確的類型註解。

39) Universe.internal 和 Context.internal 的引入。儘管很難想像這如何運作,但有可能讓巨集使用內部 API 編譯,同時與 Scala 2.10.x 和 2.11.0 相容。非常感謝 Jason Zaugg 為我們指明道路

// scala.reflect.macros.Context is available both in 2.10 and 2.11
// in Scala 2.11.0 it is deprecated
// and aliased to scala.reflect.macros.whitebox.Context
import scala.reflect.macros.Context
import scala.language.experimental.macros

// provides a source compatibility stub
// in Scala 2.10.x, it will make `import compat._` compile just fine,
// even though `c.universe` doesn't have `compat`
// in Scala 2.11.0, it will be ignored, because `import c.universe._`
// brings its own `compat` in scope and that one takes precedence
private object HasCompat { val compat = ??? }; import HasCompat._

object Macros {
  def impl(c: Context): c.Expr[Int] = {
    import c.universe._
    // enables Tree.setType that's been removed in Scala 2.11.0
    import compat._
    c.Expr[Int](Literal(Constant(42)) setType definitions.IntTpe)
  }

  def ultimateAnswer: Int = macro impl
}

40) 使用 macro-compatMacro-compat 是一個小型的程式庫,它允許您使用 Scala 2.10.x 編譯巨集,而這些巨集是針對 Scala 2.11/2 巨集 API 編寫的。它為 Scala 2.10 帶來:黑盒和白盒 Context 類型的類型別名、對巨集套件的支持、2.11 API 的轉發器以及在巨集 def 類型簽章中使用 Tree 的支援。

此頁面的貢獻者