巨集

類型提供者

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

實驗性

Eugene Burmako

類型提供者並非實作為專用的巨集風格,而是建立在 Scala 巨集已提供的功能之上。

模擬類型提供者有兩種策略:一種基於結構類型(稱為「匿名類型提供者」),一種基於巨集註解(稱為「公開類型提供者」)。前者建立在 2.10.x、2.11.x 和 2.12.x 中可用的功能上,而後者則需要巨集天堂。這兩種策略都可以用來實作下述的擦除類型提供者。

請注意,編譯和展開巨集註解都需要巨集天堂,這表示公開類型提供者的作者和使用者都必須將巨集天堂加入他們的建置中。不過,巨集註解展開後,產生的程式碼將不再有任何巨集天堂的參考,而且在編譯時或執行時都不需要巨集天堂。

最近我們在 Scala 中發表了一場關於巨集式類型提供者的演講,總結了現有技術並提供了具體範例。投影片和隨附的程式碼可以在 https://github.com/travisbrown/type-provider-examples 找到。

簡介

類型提供者是一種強類型類型橋接機制,它可以在 F# 3.0 中進行資訊豐富的程式設計。類型提供者是一種編譯時期的工具,它能夠根據描述資料來源的靜態參數產生定義及其實作。類型提供者可以在兩種模式下執行:未清除和已清除。前者類似於文字程式碼產生,因為每個產生的類型都會變成位元組碼,而在後一種情況下,產生的類型只會在類型檢查期間顯示,但在位元組碼產生之前會清除為程式設計人員提供的上限。

在 Scala 中,巨集擴充可以產生程式設計人員喜歡的任何程式碼,包括 ClassDefModuleDefDefDef 和其他定義節點,因此類型提供者的程式碼產生部分已涵蓋。記住這一點,為了模擬類型提供者,我們需要解決另外兩個挑戰

  1. 讓產生的定義公開可見(def 巨集,Scala 2.10、2.11 和 2.12 中唯一可用的巨集類型,在於其擴充的範圍有限,因此是本地的:https://groups.google.com/d/msg/scala-user/97ARwwoaq2U/kIGWeiqSGzcJ)。
  2. 讓產生的定義選擇性地可清除(Scala 支援許多語言建構的清除,例如抽象類型成員和值類別,但該機制不可擴充,這表示巨集撰寫者無法自訂它)。

匿名類型提供者

即使由 def 巨集擴充引入的定義範圍僅限於這些擴充,這些定義仍可透過轉換為結構型別來跳脫其範圍。例如,考量 h2db 巨集,它會採用連線字串並產生一個封裝指定資料庫的模組,擴充如下。

def h2db(connString: String): Any = macro ...

// an invocation of the `h2db` macro
val db = h2db("jdbc:h2:coffees.h2.db")

// expands into the following code
val db = {
  trait Db {
    case class Coffee(...)
    val Coffees: Table[Coffee] = ...
  }
  new Db {}
}

巨集擴充區塊外的任何人確實無法直接參照 Coffee 類別,但如果我們檢查 db 的型別,我們會發現一些令人著迷的事。

scala> val db = h2db("jdbc:h2:coffees.h2.db")
db: AnyRef {
  type Coffee { val name: String; val price: Int; ... }
  val Coffees: Table[this.Coffee]
} = $anon$1...

正如我們所見,當型別檢查器嘗試為 db 推斷型別時,它會採用所有對本機宣告類別的參照,並將它們替換為包含這些類別所有公開可見成員的結構型別。產生的型別擷取了已產生類別的精華,提供一個靜態型別介面給它們的成員。

scala> db.Coffees.all
res1: List[Db$1.this.Coffee] = List(Coffee(Brazilian,99,0))

這種型別提供者的方法相當簡潔,因為它可用於 Scala 的生產版本,但它有效能問題,原因在於 Scala 在編譯對結構型別成員的存取時會發出反射呼叫。有數種處理此問題的策略,但此欄位太窄無法容納它們,因此我建議您參閱 Travis Brown 的一系列精彩部落格文章,深入了解:文章 1文章 2文章 3

公開型別提供者

巨集天堂及其巨集註解的協助下,可以輕鬆產生公開可見的類別,而無需套用基於結構型別的解決方法。基於註解的解決方案非常直接,因此我不會在此多加著墨。

class H2Db(connString: String) extends StaticAnnotation {
  def macroTransform(annottees: Any*) = macro ...
}

@H2Db("jdbc:h2:coffees.h2.db") object Db
println(Db.Coffees.all)
Db.Coffees.insert("Brazilian", 99, 0)

解決抹除問題

我們尚未深入探討這一點,但有一個假設是,類型的成員和單例類型組合可以提供 F# 中已抹除類型提供者的等效項。具體來說,我們不希望抹除的類別應該宣告為一般,而應該抹除到給定上界的類別應該宣告為該上界的類型別名,參數化為承載唯一識別碼的單例類型。透過這種方法,每個新產生的類型仍然會產生額外的位元組碼到類型別名的元資料的開銷,但該位元組碼會比一個成熟類別的位元組碼小很多。此技術適用於匿名和公開類型提供者。

object Netflix {
  type Title = XmlEntity["https://.../Title".type]
  def Titles: List[Title] = ...
  type Director = XmlEntity["https://.../Director".type]
  def Directors: List[Director] = ...
  ...
}

class XmlEntity[Url] extends Dynamic {
  def selectDynamic(field: String) = macro XmlEntity.impl
}

object XmlEntity {
  def impl(c: Context)(field: c.Tree) = {
    import c.universe._
    val TypeRef(_, _, tUrl) = c.prefix.tpe
    val ConstantType(Constant(sUrl: String)) = tUrl
    val schema = loadSchema(sUrl)
    val Literal(Constant(sField: String)) = field
    if (schema.contains(sField)) q"${c.prefix}($sField)"
    else c.abort(s"value $sField is not a member of $sUrl")
  }
}

黑盒與白盒

匿名和公開類型提供者都必須是 白盒。如果您宣告一個類型提供者巨集為 黑盒,它將無法運作。

此頁面的貢獻者