-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3fda0ac
commit 0e1f1b4
Showing
1 changed file
with
133 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |