8  不透明类型

8 不透明类型

Erlang 中不透明类型的主要用例是隐藏数据类型的实现,从而在演进 API 的同时最大限度地降低破坏消费者的风险。运行时不会检查不透明性。Dialyzer 提供了一些不透明性检查,但其余部分取决于约定。

本文档通过 sets:set() 数据类型的示例解释了 Erlang 不透明性是什么(以及所涉及的权衡)。此类型**曾经**在 sets 模块中这样定义

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

OTP 24 在 此提交 中将定义更改为以下内容。

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

并且这种更改比使用 -type 而不是 -opaque 定义类型时更安全且更向后兼容。这是因为:当模块定义 -opaque 时,约定是只有定义模块应该依赖于类型的定义:其他模块不应该依赖于定义。

这意味着对 set 作为记录/元组进行模式匹配的代码在技术上违反了约定,并选择在 set() 定义更改时可能被破坏。在 OTP 24 之前,此代码会打印 ok。在 OTP 24 中,它可能会出错

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

在使用另一个模块中定义的不透明类型时,以下是一些建议

  • 不要使用模式匹配、守卫或揭示类型的函数(例如 tuple_size/1 )检查底层类型。
  • 相反,请使用模块提供的函数来处理类型。例如,sets 模块提供了 sets:new/0sets:add/2sets:is_element/2 等等。
  • sets:set(a)sets:set(a | b) 的子类型,反之则不然。通常,您可以依赖于以下属性:the_opaque(T)the_opaque(U) 的子类型,其中 T 是 U 的子类型。

在定义自己的不透明类型时,以下是一些建议

  • 由于预计消费者不会依赖于不透明类型的定义,因此您必须提供用于构造、查询和解构不透明类型实例的函数。例如,集合可以使用 sets:new/0sets:from_list/1sets:add/2 来构造,可以使用 sets:is_element/2 来查询,并可以使用 sets:to_list/1 来解构。
  • 不要使用参数位置中的类型变量定义不透明类型。这会破坏正常的预期行为,例如 my_type(a)my_type(a | b) 的子类型。
  • 规范 添加到使用不透明类型的导出函数中

请注意,不透明类型可能难以供消费者使用,因为预计消费者不会进行模式匹配,而必须使用不透明类型作者提供的函数来使用类型的实例。

此外,Erlang 中的不透明性是肤浅的:运行时不会执行不透明性检查。因此,现在集合是根据映射实现的,对集合执行 is_map/1 检查**将**通过。不透明性规则仅由约定和 Dialyzer 等其他工具强制执行,并且这种强制执行并非完全。一个有决心的 sets 消费者仍然可以揭示集合的结构,例如通过打印、序列化或将集合用作 term() 并通过 is_map/1maps:get/2 等函数检查它。此外,Dialyzer 必须进行一些 近似