Erlang's limitations in module encapsulation

I was recently looking at mocking frameworks for Erlang, to facilitate unit testing.  Mocking is a mechanism for easily producing objects and functionality to limit the scope of your tests.  For example: 

-module(a).
-export([a/1]).
a(X) ->
    b(X) * X.
b(X) ->
    X * 5.

Pretend that b/1 does something like go to a database, or connect to some other remote service, or read from a file… something that may either take a long time, or might not be available in a test environment.  For a unit test for a/1, what you’re really concerned with is testing the functionality of a/1, not b/1; to accomplish this, you’d like to mock up the functionality of b/1 to be something known, so that you can prove a/1.

What you should be able to do with mocking is something like this (there are a lot of different ways that you could define the mocking interface; I’m just picking an arbitrary one):

mock:expect( ?MODULE, b, fun(X) -> X end ).

Then you could define a unit test that says:

4 = a(2).

This makes a/1 provable through induction, because it isolates the code in a/1 – which is exactly what you want in a unit test.

Unfortunately, you can’t do this in Erlang in such a way that the unit tests don’t impact the way you write the code being tested.  This is due to a flaw in Erlang modules, and particularly, the export directive.

If you want to do mocking sample above in Erlang, the code being tested must become 

-module(a).
-export([a/1,b/1]).
a(X) ->
    a:b(X) * X.
b(X) ->
    X * 5.

This is because mocking replaces the functionality of a function with different functionality, and the Erlang compiler hard-links the call to b/1 in a/1 unless you specify the module of b/1.  This, in itself, is not a problem: the problem is that even within module a, references to functions within the same module must be exported.  That is, if a/1 wants to call a:b/1, b/1 must be exported.  Why is this a problem? Because it leads to comments like this, which you find all too commonly in Erlang projects:

-export([a/1]).
%
-------------------------------------
% WARNING: DO NOT CALL THESE FUNCTIONS
-export([b/1]).

There are a number of reasons why the authors had to export b/1. Because spawn/3 can’t call functions that aren’t exported. Because mocking requires it.  Because they want to support code reloading.  All of which are valid use cases, and all of which force developers to over-expose their API, breaking encapsulation, and suggesting that Erlang’s module handling is flawed.