summaryrefslogtreecommitdiff
path: root/system/doc/reference_manual/opaques.xml
blob: 8fa3bae3e5e6b603838cedd42c97da268a564ab9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE chapter SYSTEM "chapter.dtd">

<chapter>
  <header>
    <copyright>
      <year>2021</year>
      <year>2022</year>
      <holder>Ericsson AB. All Rights Reserved.</holder>
    </copyright>
    <legalnotice>
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
      You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

      Unless required by applicable law or agreed to in writing, software
      distributed under the License is distributed on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      See the License for the specific language governing permissions and
      limitations under the License.

    </legalnotice>

    <title>Opaques</title>
    <prepared>Max Heiber</prepared>
    <docno></docno>
    <date></date>
    <rev></rev>
    <file>opaques.xml</file>
  </header>

  <section>
    <title>Opaque Type Aliases</title>
    <p>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.
    </p>
    <p>
      This document explains what Erlang opacity is (and the trade-offs involved) via the example of OTP's
      <c>sets:set()</c>
      data type. This type
      <em>was</em>
      defined in `sets` module like this:
    </p>
    <code type="erl">-opaque set(Element) :: #set{segs :: segs(Element)}.</code>
    <p>OTP 24  changed the definition to the following, in
    <url href="https://github.com/erlang/otp/commit/e66941e8d7c47b973dff94c0308ea85a6be1958e">this commit</url>
    </p>
    <code type="erl">-opaque set(Element) :: #set{segs :: segs(Element)} | #{Element => ?VALUE}.</code>
    <p>
      And this change was safer and more backwards-compatible than if the type had been defined with
      <c>-type</c>
      instead of
      <c>-opaque</c>
      . Here's why: when a module defines an
      <c>-opaque</c>
      , the contract is that only the defining module should rely on the definition of the type: no other modules should rely on the definition.
    </p>
    <p>
      This means that code that pattern-matched on
      <c>set</c>
      as a record/tuple technically broke the contract, and opted in to being potentially broken when the definition of
      <c>set()</c>
      changed. Before OTP 24, this code printed
      <c>ok</c>
      . In OTP 24 it may error:
    </p>
    <code type="erl">
case sets:new() of
    Set when is_tuple(Set) -&gt;
        io:format(&quot;ok&quot;)
end.
    </code>
    <p>
      <strong>When working with an opaque defined in another module, here are some recommendations:</strong>
    </p>

    <list type="bulleted">
      <item>
        Don't examine the underlying type using pattern-matching, guards, or functions that reveal the type, such as
        <c>tuple_size/1</c>
        .
      </item>
      <item>
        Instead, use functions provided by the module for working with the type. For example,
        <c>sets</c>
        module provides
        <c>sets:new/0</c>
        ,
        <c>sets:add/2</c>
        ,
        <c>sets:is_element/2</c>
        , etc.
      </item>
      <item>
        <c>sets:set(a)</c>
        is a subtype of
        <c>sets:set(a | b)</c>
        and not the other way around. Generally, you can rely on the property that
        <c>the_opaque(T)</c>
        is a subtype of
        <c>the_opaque(U)</c>
        when T is a subtype of U.
      </item>
    </list>
    <p>
      <strong>When defining your own opaques, here are some recommendations:</strong>
    </p>
    <list type="bulleted">
      <item>
        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
        <c>sets:new/0</c>, <c>sets:from_list/1</c>, <c>sets:add/2</c>, queried with <c>sets:is_element/2</c>, and deconstructed with<c>sets:to_list/1</c>.
      </item>
      <item>
        Don't define an opaque with a type variable in parameter position. This breaks the normal and expected behavior that (for example)
        <c>my_type(a)</c> is a subtype of <c>my_type(a | b)</c>
      </item>
      <item>
        Add <seeguide marker="typespec">specs</seeguide> to exported functions that use the opaque type
      </item>
    </list>
    <p>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.</p>
    <p>
      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
      <c>is_map</c>
      check on a set
      <em>will</em>
      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
      <c>sets</c>
      can still do things that reveal the structure of the set, such as by printing, serializing, or using a set as
      <c>term()</c>
      and then inspecting via functions like
      <c>is_map</c>
      or
      <c>maps:get/2</c>
      . And Dialyzer must make some
      <url href="https://github.com/erlang/otp/issues/5118">approximations</url>
      . Opacity checking has limitations, but is still a vital tool in scalable Erlang development.
    </p>
  </section>
</chapter>