情境抽象

現狀批判

Scala 的隱含參數是最顯著的特點。它們是抽象脈絡的基本方式。它們代表一個統一的範例,有各種使用案例,其中包括:實作類型類別、建立脈絡、依賴注入、表達功能、計算新類型,以及證明它們之間的關係。

繼 Haskell 之後,Scala 是第二個擁有某種形式的隱含式的熱門語言。其他語言也紛紛效仿。例如 Rust 的特質Swift 的協定延伸。設計提案也出現在 Kotlin 中,例如 編譯時間相依性解析,C# 中,例如 形狀和延伸,或 F# 中,例如 特質。隱含式也是定理證明器(例如 CoqAgda)的常見功能。

即使這些設計使用截然不同的術語,它們都是項推論核心概念的變體。給定一個類型,編譯器會綜合一個具有該類型的「正規」項。Scala 以比大多數其他語言更純粹的形式體現了這個概念:一個隱含式參數直接導致一個推論的參數項,也可以明確寫出來。相比之下,基於類型類別的設計較不直接,因為它們將項推論隱藏在某種形式的類型分類之後,而且不提供明確寫出推論量(通常是字典)的選項。

鑑於項推論是產業發展的方向,而且 Scala 以非常純粹的形式具備此功能,為什麼隱含式沒有更受歡迎?事實上,公平地說,隱含式同時是 Scala 最顯著和最具爭議性的功能。我相信這是由於許多方面共同作用,使得隱含式比必要時更難學習,也更難防止濫用。

具體的批評包括

  1. 由於功能非常強大,隱含式很容易被過度使用和誤用。在幾乎所有我們討論隱含式轉換的情況下,即使在概念上有所不同,但與其他隱含式定義共用相同的語法,這個觀察都成立。例如,關於以下兩個定義

    implicit def i1(implicit x: T): C[T] = ...
    implicit def i2(x: T): C[T] = ...
    

    這些中第一個是條件式隱含,第二個是隱含轉換。條件式隱含值是表達類型類別的基石,而隱含轉換的大多數應用結果證明價值可疑。問題在於許多語言的新手從定義隱含轉換開始,因為它們易於理解,而且看似強大且方便。Scala 3 將在語言標記中放置在其他地方定義的類型之間的「未約束」隱含轉換的定義和應用。這是對過度使用隱含轉換進行反擊的有用步驟。但是,在語法上,轉換和值看起來太過相似而讓人難以接受,這個問題仍然存在。

  2. 另一種廣泛的濫用是過度依賴隱含導入。這通常會導致難以理解的類型錯誤,這些錯誤會隨著正確的導入咒語而消失,留下挫敗感。相反,由於隱含可以隱藏在很長的導入清單中的任何地方,因此很難看出程式使用了哪些隱含。

  3. 隱式定義的語法過於簡略。它只包含一個修飾詞 implicit,可以附加到大量的語言結構。對於新手來說,這是一個問題,因為它傳達的是機制而不是意圖。例如,類型類別實例是一個隱式物件或 val(如果無條件)和一個隱式 def,其中包含參照某個類別的隱式參數(如果條件)。這準確描述了隱式定義的轉換方式——只要去掉 implicit 修飾詞,就可以了!但定義意圖的提示相當間接,很容易誤讀,如上文 i1i2 的定義所示。

  4. 隱式參數的語法也有缺點。雖然隱式參數被明確指定,但參數卻沒有。向隱式參數傳遞參數看起來像一個常規應用 f(arg)。這是一個問題,因為這意味著對於在呼叫中實例化的參數可能會產生混淆。例如,在

    def currentMap(implicit ctx: Context): Map[String, Int]
    

    中不能寫 currentMap("abc"),因為字串 "abc" 被視為隱式 ctx 參數的明確參數。必須改寫為 currentMap.apply("abc"),這很奇怪且不規則。出於同樣的原因,方法定義只能有一個隱式參數區段,而且它必須始終放在最後。此限制不僅降低了正交性,還阻止了一些有用的程式結構,例如一個方法,其常規參數的類型取決於一個隱式值。最後,隱式參數必須有一個名稱,這也有些令人厭煩,儘管在許多情況下從未引用過該名稱。

  5. 隱含式對工具構成挑戰。可用的隱含式集合取決於內容,因此命令完成必須考慮內容。這在 IDE 中是可行的,但像 Scaladoc 那樣基於靜態網頁的工具只能提供近似值。另一個問題是,失敗的隱含式搜尋通常會提供非常不具體的錯誤訊息,特別是如果某些深度遞迴隱含式搜尋失敗時。請注意,Scala 3 編譯器已在錯誤診斷領域取得許多進展。如果遞迴搜尋向下失敗幾個層級,它會顯示已建構的內容和缺少的內容。此外,它會建議匯入可以將遺失的隱含式納入範圍的內容。

這些缺點都不是致命的,畢竟隱含式被廣泛使用,許多函式庫和應用程式都依賴它們。但它們加在一起,會讓使用隱含式的程式碼變得更加繁瑣且不如預期清晰。

從歷史角度來看,其中許多缺點源自於 Scala 逐漸「發現」隱含式的方式。Scala 最初只有隱含式轉換,其預期使用案例是「擴充」類別或特質,在定義之後,也就是 Scala 後續版本中隱含式類別所表達的內容。隱含式參數和實例定義後來在 2006 年出現,我們選擇類似的語法,因為它似乎很方便。出於同樣的原因,我們沒有區分隱含式匯入或引數與一般匯入或引數。

現有的 Scala 程式設計師普遍已經習慣現狀,而且認為沒有什麼需要改變。但對於新手來說,現狀是一個很大的障礙。我相信,如果我們想要克服這個障礙,我們應該退後一步,讓自己考慮一個徹底的新設計。

新設計

以下頁面介紹 Scala 中情境抽象的重新設計。它們引入了四項基本變更

  1. 給定實例 是定義可合成基本術語的新方法。它們取代了隱式定義。此提案的核心原則是,我們有一個定義可針對類型合成的術語的單一方法,而不是將 implicit 修飾詞與大量功能混用。

  2. using 子句 是隱式參數及其引數的新語法。它明確對齊參數和引數,解決了多項語言缺陷。它也允許我們在定義中使用多個 using 子句。

  3. 「給定」匯入 是一種類型的匯入選取器,專門匯入給定值,而不會匯入其他任何內容。

  4. 隱式轉換 現在表示為標準 Conversion 類別的給定實例。所有其他形式的隱式轉換都將逐步淘汰。

此部分還包含描述與情境抽象相關的其他語言功能的頁面。這些功能包括

整體而言,新設計在術語推論與語言其他部分之間取得更好的分離:定義給定值的方式只有一個,而不是採用全部都使用 implicit 修飾詞的眾多形式。引入隱含式參數和參數的方式只有一個,而不是將隱含式與一般參數混為一談。有獨立的方式來匯入給定值,不會讓它們隱藏在大量一般匯入中。而且定義隱含式轉換的方式只有一個,清楚標示為此方式,而且不需要特殊語法。

因此,此設計避免功能互動,讓語言更一致且正交。它讓隱含式更容易學習,更難濫用。它會大幅提升使用隱含式的 95% Scala 程式碼的清晰度。因此,它有潛力以有原則且容易親近的方式實現術語推論的承諾。

我們可以透過調整現有的隱含式來達成相同的目標嗎?在嘗試很長一段時間後,我現在相信這是辦不到的。

  • 首先,有些問題明顯是語法上的,需要不同的語法才能解決。
  • 其次,還有一個問題是如何遷移。我們無法在飛行途中變更規則。在語言演進的某個階段,我們需要同時容納新舊規則。透過語法變更,這很容易:以新規則引入新語法,暫時支援舊語法以利跨編譯,稍後再棄用並淘汰舊語法。維持相同的語法並未提供此途徑,事實上似乎也未提供任何可行的演進途徑
  • 第三,即使我們在遷移方面獲得某種程度的成功,我們仍然面臨如何教授此問題。我們無法讓現有的教學課程消失。幾乎所有現有的教學課程都從隱式轉換開始,而這將不復存在;它們使用正規匯入,而這將不復存在,而且它們透過將方法呼叫擴充為一般應用程式來說明具有隱式參數的方法,而這也將不復存在。這表示我們必須對所有現有的文獻和課程軟體新增修改和資格,很可能會讓初學者更加困惑,而不是減少困惑。相反地,使用新語法有一個明確的標準:任何提到implicit的書籍或課程軟體都已過時,且應更新。

目錄