How can I have a "private" Erlang module?

Viewed 238

I prefer working with files that are less than 1000 lines long, so am thinking of breaking up some Erlang modules into more bite-sized pieces.

Is there a way of doing this without expanding the public API of my library?

What I mean is, any time there is a module, any user can do module:func_exported_from_the_module. The only way to really have something be private that I know of is to not export it from any module (and even then holes can be poked).

So if there is technically no way to accomplish what I'm looking for, is there a convention? For example, there are no private methods in Python classes, but the convention is to use a leading _ in _my_private_method to mark it as private.

I accept that the answer may be, "no, you must have 4K LOC files."

4 Answers

The closest thing to a convention is to use edoc tags, like @private and @hidden.

From the docs:

@hidden

Marks the function so that it will not appear in the documentation (even if "private" documentation is generated). Useful for debug/test functions, etc. The content can be used as a comment; it is ignored by EDoc.

@private

Marks the function as private (i.e., not part of the public interface), so that it will not appear in the normal documentation. (If "private" documentation is generated, the function will be included.) Only useful for exported functions, e.g. entry points for spawn. (Non-exported functions are always "private".) The content can be used as a comment; it is ignored by EDoc.

Please note that this answer started as a comment to @legoscia's answer

Different visibilities for different methods is not currently supported.

The current convention, if you want to call it that way, is to have one (or several) 'facade' my_lib.erl module(s) that export the public API of your library/application. Calling any internal module of the library is playing with fire and should be avoided (call them at your own risk).

There are some very nice features in the BEAM VM that rely on being able to call exported functions from any module, such as

  1. Callbacks (funs/anonymous funs), MFA, erlang:apply/3: The calling code does not need to know anything about the library, just that it's something that needs to be called
  2. Behaviours such as gen_server need the previous point to work
  3. Hot reloading: You can upgrade the bytecode of any module without stopping the VM. The code server inside the VM maintains at most two versions of the bytecode for any module, redirecting external calls (those with the Module:) to the most recent version and the internal calls to the current version. That's why you may see some ?MODULE: calls in long-running servers, to be able to upgrade the code

You'd be able to argue that these points'd be available with more fine-grained BEAM-oriented visibility levels, true. But I don't think it would solve anything that's not solved with the facade modules, and it'd complicate other parts of the VM/code a great deal.

Bonus

Something similar applies to records and opaque types, records only exist at compile time, and opaque types only at dialyzer time. Nothing stops you from accessing their internals anywhere, but you'll only find problems if you go that way:

  • You insert a new field in the record, suddenly, all your {record_name,...} = break
  • You use a library that returns an opaque_adt(), you know that it's a list and use like so. The library is upgraded to include the size of the list, so now opaque_adt() is a tuple() and chaos ensues

Only those functions that are specified in the -export attribute are visible to other modules i.e "public" functions. All other functions are private. If you have specified -compile(export_all) only then all functions in module are visible outside. It is not recommended to use -compile(export_all).

I don't know of any existing convention for Erlang, but why not adopt the Python convention? Let's say that "library-private" functions are prefixed with an underscore. You'll need to quote function names with single quotes for that to work:

-module(bar).

-export(['_my_private_function'/0]).

'_my_private_function'() ->
    foo.

Then you can call it as:

> bar:'_my_private_function'().
foo

To me, that communicates clearly that I shouldn't be calling that function unless I know what I'm doing. (and probably not even then)

Related