8 不透明类型
8.1 不透明类型别名
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/0、sets:add/2、sets:is_element/2 等等。
- sets:set(a) 是 sets:set(a | b) 的子类型,反之则不然。通常,您可以依赖于以下属性:the_opaque(T) 是 the_opaque(U) 的子类型,其中 T 是 U 的子类型。
在定义自己的不透明类型时,以下是一些建议
- 由于预计消费者不会依赖于不透明类型的定义,因此您必须提供用于构造、查询和解构不透明类型实例的函数。例如,集合可以使用 sets:new/0、sets:from_list/1、sets: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/1 或 maps:get/2 等函数检查它。此外,Dialyzer 必须进行一些 近似。