Author: Richard A. O'Keefe <ok(at)cs(dot)otago(dot)ac(dot)nz>,
        Per Gustafsson <pergu(at)it(dot)uu(dot)se>
Status: Draft
Type: Standards Track
Created: 10-Aug-2007
Erlang-Version: R12B-0
Post-History:

EEP 5: More Versatile Encapsulation with export_to

Abstract

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.

Specification

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.

Motivation

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".

Rationale

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.

Backwards Compatibility

Adding an -export_to directive should be totally backwards compatible, because since it is not a legal attribute, it currently causes a syntax error.

Implementation

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.

Powered by Erlang Web