隱式解析的變更
本節說明適用於 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]
}
}
這現在會解決對 j
的 implicitly
呼叫,因為 j
的巢狀深度比 i
深。先前,這會導致歧義錯誤。先前由於 遮蔽(其中隱含的內容由巢狀定義隱藏)而導致的隱含搜尋失敗的可能性不再適用。
3. 套件前置詞不再會影響類型的隱含搜尋範圍。範例
package p
given a: A = A()
object o:
given b: B = B()
type C
a
和 b
兩個在定義 type C
的點上都可視為隱含的。不過,在套件 p
外部對 p.o.C
的參照在其隱含搜尋範圍中只有 b
,而沒有 a
。
更詳細地說,以下是構成類型隱含範圍的規則
定義:如果參照是指物件、類別、特質、抽象類型、不透明類型別名或比對類型別名,則該參照為 錨點。在 -source:3.0-migration
下,對套件和套件物件的參照才算是錨點。不透明類型別名僅在其別名可見的範圍外才算錨點。
定義:類型 T 的 錨點 是定義為下列內容的參照集合
- 如果 T 是對錨點的參照,則 T 本身加上(如果 T 的格式為 P#A)P 的錨點。
- 如果 T 是 U 的別名,則 U 的錨點。
- 如果 T 是對類型參數的參照,則其兩個邊界的錨點的聯集。
- 如果 T 是單例參照,則其基礎類型的錨點,加上(如果 T 的格式為 (P#x).type)P 的錨點。
- 如果 T 是靜態物件 o 的 this 類型 o.this,則對該物件的詞彙參照 o.type 的錨點,
- 如果 T 是其他某種此類型 P.this.type,則為 P 的錨點。
- 如果 T 是其他某種類型,則為 T 的每個組成類型的錨點的聯集。
定義:類型 T 的隱式範圍為最小的術語參考集 S,使得
- 如果 T 是對類別的參考,則 S 包含對類別的伴隨物件的參考(如果存在),以及 T 的所有父類別的隱式範圍。
- 如果 T 是對物件的參考,則 S 包含 T 本身以及 T 的所有父類別的隱式範圍。
- 如果 T 是對名為 A 的不透明類型別名的參考,則 S 包含對物件 A 的參考(如果存在),該物件定義在與類型相同的範圍內,以及 T 的底層類型或邊界的隱式範圍。
- 如果 T 是對名為 A 的抽象類型或匹配類型別名的參考,則 S 包含對物件 A 的參考(如果存在),該物件定義在與類型相同的範圍內,以及 T 的給定邊界的隱式範圍。
- 如果 T 是對形式為 p.A 的錨點的參考,則 S 也包含路徑 p 上的所有術語參考。
- 如果 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
,它直接實作否定。對於任何查詢類型 Q
,NotGiven[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 }
這裡 a
比 b
更具體,因為伴隨類別 A
是伴隨類別 B
的子類別。此外,b
比 c
更具體,因為 object B
延伸類別 C
。但 a
不比 c
更具體。這表示如果 a, b, c
都是適用的隱含式,它們的比較順序會產生差異。如果我們先比較 b
和 c
,我們會保留 b
並捨棄 c
。然後,將 a
與 b
比較,我們會保留 a
。但如果我們先比較 a
與 c
,我們會因不明確錯誤而失敗。
新規則如下:在 A
中定義的隱含式 a
比在 B
中定義的隱含式 b
更具體,如果
A
延伸B
,或A
是物件,且A
的伴隨類別延伸B
,或A
和B
是物件,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
中的底層給定值。我們透過新增下列隱含式搜尋規則來達成
- 在檢查形式為
G
的given
定義的實作時進行隱含式搜尋時given ... = ....
捨棄所有搜尋結果,這些結果會導回
G
或導回與G
擁有者相同的給定結果,且在來源中出現在G
之後。
新的行為目前已在 source.future
中啟用,且最早將在 Scala 3.6 中啟用。對於較早的來源版本,行為如下
- Scala 3.3:沒有變更
- Scala 3.4:在行為將在 3.future 中變更的地方發出警告。
- Scala 3.5:在行為將在 3.future 中變更的地方發出錯誤。
舊式隱式定義不受此變更影響。