查看源代码 不透明类型

不透明类型别名

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_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) 的子类型的正常预期行为。
  • 向使用不透明类型的导出的函数添加 specs

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

此外,Erlang 中的不透明性是表面的:运行时不强制执行不透明性检查。因此,既然集合是使用映射实现的,那么对集合执行 is_map/1 检查通过。不透明性规则仅通过约定和 Dialyzer 等附加工具来强制执行,并且这种强制并非完全。 sets 的一个坚决的消费者仍然可以揭示集合的结构,例如通过打印、序列化或将集合用作 term/0,并通过诸如 is_map/1maps:get/2 之类的函数检查它。此外,Dialyzer 必须进行一些 近似