Skip to content

Commit

Permalink
EEP 65: -import_type directive
Browse files Browse the repository at this point in the history
  • Loading branch information
ilya-klyuchnikov committed Sep 1, 2023
1 parent 3fda0ac commit 0e1f1b4
Showing 1 changed file with 133 additions and 0 deletions.
133 changes: 133 additions & 0 deletions eeps/eep-0065.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
Author: Ilya Klyuchnikov <ilya(dot)klyuchnikov(at)gmail(dot)com>
Status: Draft
Type: Standards Track
Created: 01-Sep-2023
Post-History:
****
EEP 61: `import_type` Directive
----

Abstract
========

This EEP proposes a new `-import_type(Module, [Types])` module directive
to import remote types. Imported types can be referenced the same way as local
types, eliminating the need for a module prefix.

Rationale and motivation
========

The reasoning behind the proposal aligns with the existing
`-import(Module, [Functions])` directive. The primary motivations for
introducing the directive are:

- **Simplification in modules with heavy usage of remote types**. In scenarios
where a module frequently references a specific remote type, using its fully
qualified name can become tedious. For example, the `erl_lint` module defines
a [local type anno()](https://github.com/erlang/otp/blob/5bd165d16a62d6fa30118f846d52c016141b606e/lib/stdlib/src/erl_lint.erl#L88)
to refer to the type `erl_anno:anno()`.
- **Reducing verbosity for ubiquitous types**. In certain applications
or projects, some types might be so commonly used that referencing them with
their full names becomes overly verbose. A common workaround is to place these
types in a header file and then include this header file in most of the
application's modules. An example: common types in the
[dialyzer.hrl](https://github.com/erlang/otp/blob/5bd165d16a62d6fa30118f846d52c016141b606e/lib/dialyzer/src/dialyzer.hrl#L107-L176)
header file. A downside of this approach is that it creates an additional load
for tooling (as dialyzer and type-checkers). Each module ends up with its own
local copy of the type, which can result in repeated comparisons of different
versions of the same type and slowing down the analysis.

It makes exporting and importing types symmetrical to exporting and importing
functions. Until now, there is an asymmetry: types can be exported but cannot be
imported.

Details
========

A new module directive is introduced:

-import_type(Module, [T1/A1, ..., Tk/Ak]).

Here the `Module` is an atom (the name of the module from which types are
imported), the `Ti`'s are atoms (the name of the type) and `Ai`'s are integers
(the arities). Imported types can be referenced the same way as local types -
without any module prefix.

Example:

-module(m1).
-import_type(common_types, [user/0, id/0]).

-spec get_user_id(user()) -> id().

A reference to a type should be unambiguously resolved to a locally defined
type, predefined built-in type or imported type. That imposes a few restrictions
(similar to restrictions for importing functions):

1. An import directive cannot override a built-in type.
2. A type `Ti/Ai` can be imported only once.
3. An imported type cannot be redefined locally in the module.

The restrictions are validated by the compiler (as part of the `erl_lint`
stage). It is up to tooling to check whether the imported remote type exists
and exported, the compiler doesn't check it.

Examples of restrictions:

Overriding a built-in type:

-module(m2).
-import_type(m1, [binary/0]).

error: import directive overrides auto-imported builtin type binary/0

Importing twice:

-module(m).
-import_type(m1, [user/0]).
-import_type(m2, [user/0]).

error: type user/0 already imported from m1.

Importing twice from the same module:

-module(m).
-import_type(m1, [a/0, b/0]).
-import_type(m1, [b/0, c/0]).

error: type b/0 already imported from m1

Redefining imported type:

-module(m).
-import_type(m1, [user/0]).

-type user() :: {user, binary()}.

error: defining imported type user/0

Reference implementation
========

<https://github.com/erlang/otp/pull/7618>

The implementation consists of three logical pieces:

- A change in the parser (`erl_parse`) to support the new directive.
- A change in `erl_lint` to enforce restrictions (from the _Details_ section)
- Expansion of imported types is handled by the same compiler pass that handles
function imports. So, imported functions and imported types are handled
the same way. Dialyzer already gets expanded imported types, so it
"just works".

Backwards compatibility
========

The change is backward compatible. Existing code cannot have the proposed
`import_type` attributes, since they are not parseable by the current compiler.

Copyright
=========

This document is placed in the public domain or under the CC0-1.0-Universal
license, whichever is more permissive.

0 comments on commit 0e1f1b4

Please sign in to comment.