The goal of private modules is to define a module that cannot be trivially accessed by other modules where they are not visible to.
In this proposal, private modules work by declaring exactly which other module prefixes can access it:
defmodulep MyApp.Private, visible_to: [MyApp] do
def hello do
IO.puts "hello world"
end
end
In the definition above, only MyApp
and modules nested under it can access MyApp.Private
. In this other example:
defmodulep MyApp.Nested.Schema, visible_to: [MyApp.Nested] do
def hello do
IO.puts "hello world"
end
end
only modules in MyApp.Nested
and under it can access MyApp.Nested.Schema
.
To access a private module, you must explicitly require and alias it:
defmodule MyApp.Other do
require MyApp.Private, as: Private
Private.hello
end
The require is necessary to validate the visibility rules. The alias is required to give the private module a proper name (as we will learn later on, private modules live in different namespaces).
Private modules can be arbitrarily nested too:
defmodulep MyApp.Private, visible_to: [MyApp] do
defmodulep Nested, visible_to: [MyApp] do
def hello do
IO.puts "hello world"
end
end
end
Requiring MyApp.Private
does not automatically require MyApp.Private.Nested
. It still need to be explicitly required either directly:
require MyApp.Private.Nested, as: Nested
If you have already required Private
, you can also require Nested
from the Private
alias:
require MyApp.Private, as: Private
require Private.Nested, as: Nested
Nesting
defmodulep
works as defmodule
as it can be accessed directly following its definition:
defmodule Foo do
defmodulep Bar, visible_to: [MyApp] do
...
end
Bar # We can access bar here even if not in visible_to
end
In other words, a more correct description of defmodulep
is that it is visible to any following module declared in the same file or to any module declared in visible_to
. In fact, :visible_to
may be skipped for nested private modules which means they are only accessible to the following modules in the same file.
Testing
In order to test a private module, you need to make sure the private module is visible to the test module. Since most private modules are visible to their own rootname, testing just works if you follow Elixir’s testing conventions. For instance, a private module MyApp.Foo.Bar
is likely visible to MyApp
or MyApp.Foo
, which means the default test module, which is MyApp.Foo.BarTest
, should have access to the private module. In other words, the following code should work just fine:
# lib/my_app/foo/bar.ex
defmodulep MyApp.Foo.Bar, visible_to: MyApp.Foo do
...
end
# test/my_app/foo/bar_test.exs
defmodule MyApp.Foo.BarTest do
use ExUnit.Case
require MyApp.Foo.Bar, as: Bar
...
end
Inspecting private modules
Private modules work by being assigned a different naming structure. If you define a private module Foo.Bar
, it will actually be compiled as :"modulep_DDD_Elixir.Foo.Bar"
, where DDD
will be a arbitrarily assigned number, instead of the usual Elixir.Foo.Bar
. The number is arbitrary to discourage developers from accessing the underlying module directly, as this number may change at any time. The only way to safely access a private module is by requiring and aliasing it first.
Proof of Concept
I have written a proof of concept that is "ready to use today"™ for those willing to try this idea out:
However, the proof of concept has certain limitations:
-
Since we can’t change the behaviour of
require
, the library introduces arequirep
to require private modules. -
If you define
defmodulep Foo
and thendefmodule Foo
, the proof of concept won’t warn. -
If you invoke
SomePrivateModule.foo
without requiring it, the error message says the module does not exist, without giving any hints the module is actually private (this may or may not be a feature). -
Private modules appear literally as
:"modulep_DDD_Elixir.Foo.Bar"
but it could show up asFoo.Bar
when inspected by updating theInspect
implementation for atoms -
If you define a module
defmodule Public
nested insidedefmodulep Private
,Public
cannot be accessed directly but only viarequirep Private, as: Private
and then by callingPrivate.Public
. This will be fixed if we add this to Elixir by makingModule.concat/1
to be aware ofmodulep_DDD_
prefixes.
All of those limitations could be addressed by adding defmodulep
to Elixir.
Your turn
I would love to hear feedback on:
-
The feature
-
Implementation details and concerns on this area
-
The proof of concept
Also, I would love examples of how other languages tackle private modules. A common implementation is to have the visibility of your modules associated to the “idea of a package” but Elixir does not quite have the concept of a package. Elixir does provide the idea of “applications” but they are define only after the code is compiled. That’s why the “package” approach has been ruled out in favor of a explicit visible_to
control.