summaryrefslogtreecommitdiff
path: root/system/doc/reference_manual/opaques.xml
diff options
context:
space:
mode:
Diffstat (limited to 'system/doc/reference_manual/opaques.xml')
-rw-r--r--system/doc/reference_manual/opaques.xml141
1 files changed, 141 insertions, 0 deletions
diff --git a/system/doc/reference_manual/opaques.xml b/system/doc/reference_manual/opaques.xml
new file mode 100644
index 0000000000..8fa3bae3e5
--- /dev/null
+++ b/system/doc/reference_manual/opaques.xml
@@ -0,0 +1,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>