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