8 Opaques
8.1 Opaque Type Aliases
The main use case for opacity in Erlang is to hide the implementation of a data type, enabling evolving the API while minimizing the risk of breaking consumers. The runtime does not check opacity. Dialyzer provides some opacity-checking, but the rest is up to convention.
This document explains what Erlang opacity is (and the trade-offs involved) via the example of OTP's sets:set()
data type. This type was defined in `sets` module like this:
-opaque set(Element) :: #set{segs :: segs(Element)}.
OTP 24 changed the definition to the following, in this commit
-opaque set(Element) :: #set{segs :: segs(Element)} | #{Element => ?VALUE}.
And this change was safer and more backwards-compatible than if the type had been defined with -type
instead of -opaque
. Here's why: when a module defines an -opaque
, the contract is that only the defining module should rely on the definition of the type: no other modules should rely on the definition.
This means that code that pattern-matched on set
as a record/tuple technically broke the contract, and opted in to being potentially broken when the definition of set()
changed. Before OTP 24, this code printed ok
. In OTP 24 it may error:
case sets:new() of Set when is_tuple(Set) -> io:format("ok") end.
When working with an opaque defined in another module, here are some recommendations:
- Don't examine the underlying type using pattern-matching, guards, or functions that reveal the type, such as
tuple_size/1
. - Instead, use functions provided by the module for working with the type. For example,
sets
module providessets:new/0
,sets:add/2
,sets:is_element/2
, etc. -
sets:set(a)
is a subtype ofsets:set(a | b)
and not the other way around. Generally, you can rely on the property thatthe_opaque(T)
is a subtype ofthe_opaque(U)
when T is a subtype of U.
When defining your own opaques, here are some recommendations:
- Since consumers are expected to not rely on the definition of the opaque type, you must provide functions for constructing and querying/deconstructing intances of your opaque type. For example, sets can be constructed with
sets:new/0
,sets:from_list/1
,sets:add/2
, queried withsets:is_element/2
, and deconstructed withsets:to_list/1
. - Don't define an opaque with a type variable in parameter position. This breaks the normal and expected behavior that (for example)
my_type(a)
is a subtype ofmy_type(a | b)
- Add
specs
to exported functions that use the opaque type
Note that opaques can be harder to work with for consumers, since the consumer is expected not to pattern-match and must instead use functions that the author of the opaque type provides to use instances of the type.
Also, opacity in Erlang is skin-deep: the runtime does not enforce opacity-checking. So now that sets are implemented in terms of maps, an is_map
check on a set will pass. The opacity rules are only enforced by convention and by additional tooling such as Dialyzer. And this enforcement is not total: For example, determined consumer of sets
can still do things that reveal the structure of the set, such as by printing, serializing, or using a set as term()
and then inspecting via functions like is_map
or maps:get/2
. And Dialyzer must make some approximations
. Opacity checking has limitations, but is still a vital tool in scalable Erlang development.
© 2010–2022 Ericsson AB
Licensed under the Apache License, Version 2.0.