Ilya Klyuchnikov <ilya(dot)klyuchnikov(at)gmail(dot)com>
Standards Track

EEP 65: 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() 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 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.


-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:

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

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

Importing twice:

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

error: type user/0 already imported from m1.

Importing twice from the same module:

-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:

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

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

error: defining imported type user/0

Reference implementation #

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.