export_to
#
This EEP describes a new directive called export_to
which allows a
module to specify exactly which other modules that can call a function
defined in the module. This provides a very fine grained primitive for
encapsulation. Allowing the programmer to control more directly how
his code should be used.
This is an idea originally proposed by Richard O’Keefe.
This is the syntax for the -export_to
directive:
-export_to([m,...], [f/a,...]).
where [m,...]
is a list of module names and ``[f/a,…]’’
is a list of function name/arity pairs.
This means that the function f/a
can be called from the module
m
. Whether the call is a static call, a dynamic call,
or an apply should not matter.
Except for the restriction to a specified set of modules,
these functions should act like functions exported to the
world using -export
, i.e., calls to these functions
should always invoke the latest version of the function.
When a list has only one element, it may be abbreviated to just that element, so
-export_to(m, [f/a,...]).
-export_to(m, f/a).
-export_to([m,...], f/a).
are allowed abbreviations.
A function may be explicitly exported to any number of modules.
An Erlang compiler should allow a function to be both explicitly
exported to one or more modules with -export_to
and also
exported to the world with -export
, in order to ease the
transition, but should by default warn about this.
The existing export_all
flag should also be compatible with
explicit exports.
Modules in Erlang have several roles. A module is the unit of compilation and code reloading. It is also the unit of encapsulation, because the only way to hide a function from being called by any other function is to make it a local function in a module. A module also defines a separate namespace for functions. This wealth of roles for the module makes it difficult to structure Erlang applications in a way that makes them well encapsulated while keeping modules small and focused.
Most applications written in Erlang consist of several smaller modules, and though the application has a public API which is a subset of the exported functions of the modules, exactly which functions are part of the public API can only be specified in comments or documentation since functions are either exported so that anyone can call them, or not exported meaning that they can only be called inside the module, but sometimes we want to have more control than this, e.g., this function should only be called from other modules in this application, or this function should only be called from the shell.
The -export_to
directive lets the programmer
express such restrictions, which the runtime system then enforces,
so that readers can trust them.
The -export_to
directive is not meant to replace
the -export
directive, but to be an alternative when the
programmer knows all intended collaborators.
This feature was originally inspired by the way the programming language Eiffel controls the visibility of a class’s features to other classes, so Bertrand Meyer deserves the credit. This means, of course, that the idea is “proven technology”.
There are some choices in designing the -export_to
syntax,
for example should m be allowed to be a list of modules or
should we have an export_to
list where each entry is a module,
function/arity pair.
One reason to use the suggested syntax is that it reads pretty easily as:
export to module m
this list of functions [f/a]
One way to think about this is that there is an “export” matrix where the rows are indexed by modules and the columns are indexed by local functions. Sometimes it is convenient to slice it by rows (this behaviour module may call these callbacks) and sometimes it is convenient to slice it by columns (this utility function may be called by all these modules in my application). The original design focus was on callbacks so that they could be exported to a behaviour without exporting them to the world. Since then it has become clear that exporting to an application is also a convenient thing. So let us allow programmers to write the matrix whatever way most clearly expresses their intentions.
Another issue is whether we should have some syntactic sugar for specifying common export patterns such as exporting a set of functions to all the modules in an application or exporting a function in order to make it possible to apply the function or to make it possible to update the code of the function.
In fact exporting to an application is already easy with this
proposal. When you develop an application foo, create a
file foo_modules.hrl
with the contents:
-define(_FOO_`_MODULES`,
[ mod1
, mod2
...
]).
Then within a file you may write
-include('_foo_`_modules.hrl').
-export_to(_FOO_`_MODULES`, [f/a,...]).
Further additions to this proposal may be worth discussing once we have some experience with this basic building block.
One issue which perhaps deserves comment is that calling a restricted-export function is just like any other remote call. These seems to be no reason to make it different. In particular, if module A initially exports a function to the world, and module B calls it, and then the author of module A decides to restrict it to B, B should not change.
Adding an -export_to
directive should be totally backwards
compatible, because since it is not a legal attribute, it currently
causes a syntax error.
This feature has not been implemented yet, but here are some goals that we think the implementation should fulfill:
Ordinary static calls to an -export_to
function should cost the
same as calls to other -export_to
functions.
The performance of other calls should not be affected by the
introduction of -export_to
calls.
The space cost should be O(1) per (m,f,a) matrix entry.
These can be archived by putting most of the machinery to handle this feature in the loader and only using dynamic checks for dynamic calls.