在 GitHub 上編輯此頁面

隱式解析的變更

本節說明適用於 Scala 3 中新的 given 和舊式 implicit 的隱式解析變更。隱式解析使用新的演算法,更積極地快取隱式結果以提升效能。語言層級上也有一些影響隱式的變更。

1. 隱式值的類型和隱式方法的結果類型必須明確宣告。只有在仍可推論類型的區域區塊中的值例外

class C {

    val ctx: Context = ...        // ok

    /*!*/ implicit val x = ...    // error: type must be given explicitly

    /*!*/ implicit def y = ...    // error: type must be given explicitly
  }
  val y = {
    implicit val ctx = this.ctx // ok
    ...
  }

2. 選擇隱式時現在會考量巢狀結構。例如,考慮下列場景

def f(implicit i: C) = {
    def g(implicit j: C) = {
      implicitly[C]
    }
  }

這現在會解決對 jimplicitly 呼叫,因為 j 的巢狀深度比 i 深。先前,這會導致歧義錯誤。先前由於 遮蔽(其中隱含的內容由巢狀定義隱藏)而導致的隱含搜尋失敗的可能性不再適用。

3. 套件前置詞不再會影響類型的隱含搜尋範圍。範例

package p

  given a: A = A()

  object o:
    given b: B = B()
    type C

ab 兩個在定義 type C 的點上都可視為隱含的。不過,在套件 p 外部對 p.o.C 的參照在其隱含搜尋範圍中只有 b,而沒有 a

更詳細地說,以下是構成類型隱含範圍的規則

定義:如果參照是指物件、類別、特質、抽象類型、不透明類型別名或比對類型別名,則該參照為 錨點。在 -source:3.0-migration 下,對套件和套件物件的參照才算是錨點。不透明類型別名僅在其別名可見的範圍外才算錨點。

定義:類型 T錨點 是定義為下列內容的參照集合

  1. 如果 T 是對錨點的參照,則 T 本身加上(如果 T 的格式為 P#AP 的錨點。
  2. 如果 TU 的別名,則 U 的錨點。
  3. 如果 T 是對類型參數的參照,則其兩個邊界的錨點的聯集。
  4. 如果 T 是單例參照,則其基礎類型的錨點,加上(如果 T 的格式為 (P#x).typeP 的錨點。
  5. 如果 T 是靜態物件 o 的 this 類型 o.this,則對該物件的詞彙參照 o.type 的錨點,
  6. 如果 T 是其他某種此類型 P.this.type,則為 P 的錨點。
  7. 如果 T 是其他某種類型,則為 T 的每個組成類型的錨點的聯集。

定義:類型 T隱式範圍為最小的術語參考集 S,使得

  1. 如果 T 是對類別的參考,則 S 包含對類別的伴隨物件的參考(如果存在),以及 T 的所有父類別的隱式範圍。
  2. 如果 T 是對物件的參考,則 S 包含 T 本身以及 T 的所有父類別的隱式範圍。
  3. 如果 T 是對名為 A 的不透明類型別名的參考,則 S 包含對物件 A 的參考(如果存在),該物件定義在與類型相同的範圍內,以及 T 的底層類型或邊界的隱式範圍。
  4. 如果 T 是對名為 A 的抽象類型或匹配類型別名的參考,則 S 包含對物件 A 的參考(如果存在),該物件定義在與類型相同的範圍內,以及 T 的給定邊界的隱式範圍。
  5. 如果 T 是對形式為 p.A 的錨點的參考,則 S 也包含路徑 p 上的所有術語參考。
  6. 如果 T 是其他某種類型,則 S 包含 T 的所有錨點的隱式範圍。

4. 模糊錯誤的處理方式已變更。如果在隱式搜尋的某個遞迴步驟中遇到模糊性,則模糊性會傳播到呼叫者。

範例:假設您有以下定義

class A
  class B extends C
  class C
  implicit def a1: A
  implicit def a2: A
  implicit def b(implicit a: A): B
  implicit def c: C

以及查詢 implicitly[C]

此查詢現在會被分類為不明確。這很有道理,畢竟有兩個可能的解法,b(a1)b(a2),沒有哪一個比另一個好,而且兩個都比第三個解法 c 好。相比之下,Scala 2 會將搜尋 A 視為不明確而拒絕,然後將查詢 b(implicitly[A]) 分類為正常失敗,這表示替代方案 c 將會被選為解法!

Scala 2 在不明確性方面的令人費解行為已被利用來實作隱式解析中「否定」搜尋的類比,其中如果其他查詢 Q2 成功,查詢 Q1 會失敗,如果 Q2 失敗,Q1 會成功。有了新的清理行為,這些技術就不再管用了。但現在有一個新的特殊類型 scala.util.NotGiven,它直接實作否定。對於任何查詢類型 QNotGiven[Q] 僅在對 Q 的隱式搜尋失敗時才會成功。

5. 發散錯誤的處理方式也已改變。發散隱式會被視為正常失敗,之後仍會嘗試其他方案。這也很有道理:遇到發散隱式表示我們假設在對應路徑上找不到有限解法,但仍可以嘗試其他路徑。相比之下,Scala 2 中大多數(但不是全部)發散錯誤會終止整個隱式搜尋。

6. Scala 2 將優先較低層級的隱式轉換與呼叫依名稱參數,相對於呼叫依值參數的隱式轉換。Scala 3 取消此區別。因此,下列程式碼片段在 Scala 3 中會產生歧義

implicit def conv1(x: Int): A = new A(x)
  implicit def conv2(x: => Int): A = new A(x)
  def buzz(y: A) = ???
  buzz(1)   // error: ambiguous

7. 選擇一組超載或隱式替代方案中最特定替代方案的規則經過調整,以考量內容參數。其他條件相同的情況下,採用某些內容參數的替代方案被視為比不採用的替代方案不特定。如果兩個替代方案都採用內容參數,我們會試著選擇它們,就像它們是具有常規參數的方法。 SLS §6.26.3 中的下列段落受到此變更影響

原始版本

替代方案 A 比替代方案 B 更特定,如果 A 相對於 B 的相對權重高於 B 相對於 A 的相對權重。

修改版本

替代方案 A 比替代方案 B 更特定,如果

  • A 相對於 B 的相對權重高於 B 相對於 A 的相對權重,或
  • 相對權重相同,且 A 沒有採用隱式參數,但 B 有,或
  • 相對權重相同,A 和 B 都採用隱式參數,且如果將任一替代方案中的所有隱式參數替換為常規參數,則 A 比 B 更特定。

8. 根據繼承深度對隱式進行先前的消除歧義經過調整,使其具有傳遞性。傳遞性對於保證搜尋結果與編譯順序無關非常重要。以下是一個先前的規則違反傳遞性的場景

class A extends B
  object A { given a ... }
  class B
  object B extends C { given b ... }
  class C { given c }

這裡 ab 更具體,因為伴隨類別 A 是伴隨類別 B 的子類別。此外,bc 更具體,因為 object B 延伸類別 C。但 a 不比 c 更具體。這表示如果 a, b, c 都是適用的隱含式,它們的比較順序會產生差異。如果我們先比較 bc,我們會保留 b 並捨棄 c。然後,將 ab 比較,我們會保留 a。但如果我們先比較 ac,我們會因不明確錯誤而失敗。

新規則如下:在 A 中定義的隱含式 a 比在 B 中定義的隱含式 b 更具體,如果

  • A 延伸 B,或
  • A 是物件,且 A 的伴隨類別延伸 B,或
  • AB 是物件,B 未從基底類別繼承任何隱含式成員 (*),且 A 的伴隨類別延伸 B 的伴隨類別。

條件 (*) 是新的。這有必要確保定義的關係具有遞移性。

[//]: # todo: expand with precise rules

9. 目前在 -source future 中啟用下列變更

隱含式解析現在避免產生遞迴給定值,這可能會在執行階段導致無限迴圈。以下是範例

object Prices {
  opaque type Price = BigDecimal

  object Price{
    given Ordering[Price] = summon[Ordering[BigDecimal]] // was error, now avoided
  }
}

先前,隱含式解析會將 summon 解析為 Price 中的給定值,導致無限迴圈(這種情況會發出警告)。現在我們改用 BigDecimal 中的底層給定值。我們透過新增下列隱含式搜尋規則來達成

  • 在檢查形式為 Ggiven 定義的實作時進行隱含式搜尋時
    given ... = ....
    

    捨棄所有搜尋結果,這些結果會導回 G 或導回與 G 擁有者相同的給定結果,且在來源中出現在 G 之後。

新的行為目前已在 source.future 中啟用,且最早將在 Scala 3.6 中啟用。對於較早的來源版本,行為如下

  • Scala 3.3:沒有變更
  • Scala 3.4:在行為將在 3.future 中變更的地方發出警告。
  • Scala 3.5:在行為將在 3.future 中變更的地方發出錯誤。

舊式隱式定義不受此變更影響。