巨集

隱式巨集

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

實驗性質

Eugene Burmako

隱式巨集自版本 2.10.0 起作為 Scala 的實驗性質功能發布,包括即將推出的 2.11.0,但需要 2.10.2 中的重大錯誤修正才能完全運作。隱式巨集不需要巨集天堂才能運作,無論是在 2.10.x 還是 2.11 中。

隱含巨集的擴充,稱為 fundep 具現化,在 2.10.0 至 2.10.4 中不可用,但已在 巨集天堂、Scala 2.10.5 和 Scala 2.11.x 中實作。請注意,在 2.10.0 至 2.10.4 中,fundep 具現化巨集的擴充需要巨集天堂,這表示您的使用者必須將巨集天堂新增至其建置才能使用您的 fundep 具現化。不過,在 fundep 具現化擴充後,產生的程式碼將不再有任何巨集天堂的參照,且在編譯時或執行時都不需要它的存在。另請注意,在 2.10.5 中,fundep 具現化巨集的擴充可以在沒有巨集天堂的情況下進行,但您的使用者必須啟用 -Yfundep-materialization 編譯器旗標。

隱含巨集

型別類別

以下範例定義 Showable 型別類別,它抽象出美化列印策略。隨附的 show 方法採用兩個參數:一個明確的參數(目標),和一個隱含的參數(承載 Showable 的執行個體)。

trait Showable[T] { def show(x: T): String }
def show[T](x: T)(implicit s: Showable[T]) = s.show(x)

在如此宣告後,show 可以只提供目標就呼叫,而 scalac 會嘗試根據目標的型別,從呼叫站點的範圍推論對應的型別類別執行個體。如果範圍內有相符的隱含值,它將被推論出來,編譯就會成功,否則會發生編譯錯誤。

implicit object IntShowable extends Showable[Int] {
  def show(x: Int) = x.toString
}
show(42) // "42"
show("42") // compilation error

樣板程式碼的擴散

型別類別眾所周知的問題之一,特別是在 Scala 中,是類似型別的執行個體定義通常非常相似,這會導致樣板程式碼擴散。

例如,對於許多物件,美化列印表示列印其類別名稱和欄位的名稱和值。即使這種和類似的範例非常簡潔,在實務上通常不可能簡潔地實作它們,因此程式設計師被迫一再重複自己。

class C(x: Int)
implicit def cShowable = new Showable[C] {
  def show(c: C) = "C(" + c.x + ")"
}

class D(x: Int)
implicit def dShowable = new Showable[D] {
  def show(d: D) = "D(" + d.x + ")"
}

這個使用案例可以用執行時期反射來實作,但反射通常不是因為擦除而太不精確,就是因為它帶來的負擔而太慢。

也存在基於類型層級程式設計的泛型程式設計方法,例如,Lars Hupel 介紹的 TypeClass 類型類別技術,但與手動編寫的類型類別實例相比,它們的效能也受到影響。

隱式具體化器

使用隱式巨集可以消除樣板,完全移除手動定義類型類別實例的需求,而且不犧牲效能。

trait Showable[T] { def show(x: T): String }
object Showable {
  implicit def materializeShowable[T]: Showable[T] = macro ...
}

程式設計師不是撰寫多個實例定義,而是在 Showable 類型類別的伴隨物件中定義單一 materializeShowable 巨集。伴隨物件的成員屬於關聯類型類別的隱式範圍,這表示在程式設計師未提供 Showable 的明確實例時,將會呼叫具體化器。具體化器在被呼叫時,可以取得 T 的表示形式,並產生 Showable 類型類別的適當實例。

隱式巨集的一項優點是,它們可以無縫地融入現有的隱式搜尋基礎架構。Scala 隱式的標準功能,例如多參數性和重疊實例,都可以供隱式巨集使用,而程式設計師不必特別費力。例如,可以為漂亮可列印元素的清單定義非巨集漂亮列印器,並讓它與基於巨集的具體化器透明整合。

implicit def listShowable[T](implicit s: Showable[T]) =
  new Showable[List[T]] {
    def show(x: List[T]) = { x.map(s.show).mkString("List(", ", ", ")")
  }
}
show(List(42)) // prints: List(42)

在這種情況下,所需的實例 Showable[Int] 會由上面定義的具象化巨集產生。因此,透過讓巨集隱含,它們可用於自動化類型類別實例的具象化,同時與非巨集隱含無縫整合。

Fundep 具象化

問題陳述

促成 fundep 具象化的使用案例是由 Miles Sabin 和他的 shapeless 函式庫提供的。在 shapeless 的舊版本(2.0.0 之前),Miles 定義了 Iso 特質,它代表類型之間的同構。 Iso 可用於將案例類別對應到元組,反之亦然(實際上,shapeless 使用 Iso 在案例類別和 HList 之間進行轉換,但為了簡化起見,我們使用元組)。

trait Iso[T, U] {
  def to(t: T) : U
  def from(u: U) : T
}

case class Foo(i: Int, s: String, b: Boolean)
def conv[C, L](c: C)(implicit iso: Iso[C, L]): L = iso.to(c)

val tp  = conv(Foo(23, "foo", true))
tp: (Int, String, Boolean)
tp == (23, "foo", true)

如果我們嘗試為 Iso 編寫隱含具象化,我們將會遇到阻礙。在類型檢查 conv 等方法的應用時,scalac 必須推論類型參數 L,它對此一無所知(這不足為奇,因為這是特定領域的知識)。因此,當我們定義一個隱含巨集來合成 Iso[C, L] 時,scalac 會在展開巨集之前,樂於將 L 推論為 Nothing,然後一切都將分崩離析。

建議的解決方案

正如 https://github.com/scala/scala/pull/2499 所示,針對所概述問題的解決方案極為簡單且優雅。

在 2.10 中,我們不允許巨集應用程式擴充,直到所有類型引數都推論出來。然而,我們不必這麼做。類型檢查器可以推論出它可能推論出的所有內容(例如在執行範例中,C 會推論為 Foo,而 L 則會保持未推論),然後停止。在那之後,我們擴充巨集,然後使用擴充類型來協助類型檢查器處理先前未確定的類型引數,繼續進行類型推論。這是在 Scala 2.11.0 中實作的方式。

可以在我們的 files/run/t5923c 測試中找到此技術實際應用的說明。請注意一切有多麼簡單。materializeIso 隱含巨集只取其第一個類型引數,並使用它來產生擴充。我們不需要理解第二個類型引數(尚未推論出來),我們不需要與類型推論互動 - 所有事情都會自動發生。

請注意,有一個有趣的警告與我們計畫稍後處理的 Nothing。

黑箱與白箱

香草實體化器(在本文檔的第一部分中介紹)可以同時是 黑箱白箱

黑盒子和白盒子具象化器之間有明顯的區別。在黑盒子隱式巨集的擴展中出現錯誤(例如明確的 c.abort 呼叫或擴展類型檢查錯誤)將會產生編譯錯誤。在白盒子隱式巨集的擴展中出現錯誤只會從當前隱式搜尋中的隱式候選清單中移除巨集,而不會向使用者報告實際錯誤。這會產生權衡:黑盒子隱式巨集具有更好的錯誤報告功能,而白盒子隱式巨集則更靈活,必要時能夠動態關閉自身。

基金會具象化器必須是白盒子的。如果您將基金會具象化器宣告為黑盒子,它將無法運作。

此頁面的貢獻者