檢視原始碼 不透明型 (Opaques)

不透明型別名 (Opaque Type Aliases)

在 Erlang 中使用不透明型的主要目的是隱藏資料類型的實作細節,使 API 能夠在演進的同時,將破壞使用者的風險降至最低。執行期不會檢查不透明性。 Dialyzer 提供一些不透明性檢查,但其餘的則取決於慣例。

本文透過 sets:set() 資料類型的範例,說明 Erlang 的不透明性是什麼(以及所涉及的權衡)。此類型sets 模組中定義如下

-opaque set(Element) :: #set{segs :: segs(Element)}.

OTP 24 在 此 commit 中將定義更改為以下內容。

-opaque set(Element) :: #set{segs :: segs(Element)} | #{Element => ?VALUE}.

而且,如果使用 -opaque 而非 -type 定義類型,此變更會更安全且更具回溯相容性。原因如下:當模組定義 -opaque 時,合約規定只有定義模組應依賴該類型的定義:沒有其他模組應依賴該定義。

這表示在 set 上進行模式比對 (pattern-matched) 作為記錄/元組的程式碼,實際上是違反了合約,並選擇在 set() 的定義變更時可能被破壞。在 OTP 24 之前,此程式碼會印出 ok。在 OTP 24 中,它可能會發生錯誤

case sets:new() of
    Set when is_tuple(Set) ->
        io:format("ok")
end.

使用在另一個模組中定義的不透明型時,以下是一些建議

  • 請勿使用模式比對、防護 (guards) 或揭露類型的函式(例如 tuple_size/1)來檢查底層類型。
  • 相反地,請使用模組提供的函式來處理該類型。例如,sets 模組提供 sets:new/0sets:add_element/2sets:is_element/2 等等。
  • sets:set(a)sets:set(a | b) 的子類型,反之則不然。一般來說,您可以仰賴當 T 是 U 的子類型時,the_opaque(T)the_opaque(U) 的子類型這一特性。

在定義您自己的不透明型時,以下是一些建議

  • 由於預期使用者不會依賴不透明類型的定義,因此您必須提供用於建構、查詢和解構不透明類型實例的函式。例如,集合可以使用 sets:new/0sets:from_list/1sets:add_element/2 建構,使用 sets:is_element/2 查詢,並使用 sets:to_list/1 解構。
  • 請勿在參數位置使用類型變數定義不透明型。這會破壞正常且預期的行為,例如 my_type(a)my_type(a | b) 的子類型
  • 規格 新增至使用不透明類型的已匯出函式

請注意,對於使用者來說,不透明型可能更難處理,因為預期使用者不會進行模式比對,而必須使用不透明類型作者提供的函式來使用該類型的實例。

此外,Erlang 中的不透明性是表面上的:執行期不會強制執行不透明性檢查。因此,既然集合是以映射 (maps) 的形式實作的,對集合進行 is_map/1 檢查會通過。不透明性規則僅透過慣例和額外的工具(例如 Dialyzer)強制執行,而且此強制執行並非完全。即使是 sets 的堅決使用者,仍然可以揭露集合的結構,例如透過列印、序列化,或使用集合作為 term/0,並透過 is_map/1maps:get/2 之類的函式進行檢查。此外,Dialyzer 必須進行一些近似值