josevalim
moderators note:
A conclusion by @josevalim has been drawn in Proposal: Private modules (general discussion) - #143 by josevalim
While Elixir has private functions, it does not have the concepts of private modules. This makes it harder for applications and libraries to define clear boundaries and communicate intent clearly.
As an example, when Elixir v1.7 was released, it broke some libraries that were using Elixir’s private APIs. This gives an impression of instability and immutarity in the ecosystem. Even more worrying, is that this practice in the long term can be really harmful as systems grow in size. If we, as a community, fail to define boundaries and fail to respect compatibility, updating only a small part of the system becomes impossible, because a minimal change breaks many unwarranted things along the way. It usually goes like this: let’s update Elixir! Unfortunately, updating Elixir breaks package X because X used a private API. So we have to update package X too but wait! That breaks Y and Z. Soon you find yourself having to update the whole system at once.
For these reasons, it is desirable to have a better way to outline boundaries and communicate intent. This is not only useful when working with dependencies. Even within the same library or application, developers can use well-defined boundaries to better organize their codebase and reveal intent to their coworkers.
However, one of the questions on this topic is how strict does the private module system has to be as there are many situations we would like to bypass it.
As @scarfacedeb mentioned in another thread:
I can think of at least 2 uses of open private modules:
As @JEG2 said, sometimes you have to use private modules in IEx in prod. You may encounter an unexpected error that you didn’t anticipate in your code and the quickest way to debug it is to call internal modules by hand and check the results. You could also use tracing in these cases, but I don’t see why we can’t have both.
When I’m learning how a new library (or app) works, I often call its internal modules directly to experiment and get a better idea how they work under the hood. Now it’s easy to do in iex and it doesn’t require to now about any new concepts (such as private modules, their visibility, etc).
Making code easy to explore is useful in production and while learning too.
With this in mind, this proposal is going to highlight four possible implementations for further discussion. Before we get to the possible implementation, we need to establish some common ground. Note the APIs in this proposal are not final and are meant to be examples. Once an approach is chosen, we can have a separate discussion to refine its APIs.
Best-effort warnings
One possible implementation of “private modules” is to provide best-effort warnings. This would work by annotating the visibility of a module, such as:
defmodule MyApp.Private do
@module_visible_to [MyApp]
end
Now invoking MyApp.Private outside of MyApp will emit a warning that the module is private and may not be accessible externally.
It is important to note that, when code is compiled, Elixir does not actually guarantee the module you are calling exist. For example, if you have this function:
defmodule Foo do
def bar, do: Bar.baz
end
The code will compile even if Bar is not defined. While mix does warn in cases like this, those warnings are “best-effort”. For example, the code below, while semantically the same to the code above, won’t warn:
defmodule Foo do
def bar do
mod = Bar
mod.baz
end
end
which means that “best-effort warnings” for private modules can be easily bypassed by doing:
defmodule Foo do
def bar do
mod = MyApp.Private
mod.baz
end
end
Therefore, the only way we could consistently and constantly warning when breaking a private module boundary, is if the private modules are required (via require/2) before they are used. Otherwise, we can only provide best-effort warnings, which are extremely easy to bypass and may not display as frequently.
Guaranteed warnings/errors (defmodulep)
If we want to have guaranteed warnings, private modules must be explicitly required before usage. One possible implementation of such mechanisms is to introduce a defmodulep construct, that defines a module in a separate namespace:
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. 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 bring the private module to the current namespace. The require+alias mechanism is essential to this alternative.
If we decide to go on the defmodulep route, we have three options:
-
Modules must be explicitly required+aliased and it will error if you break its boundaries. The namespace the module will be assigned to is private, which means you have no official ways of accessing a private module beyond its original intent.
-
Modules must be explicitly required+aliased and it will error if you break its boundaries. However, the namespace the module will be assigned to is public, which means you can access it directly, without any visibility check, by using its long name. For example,
defmodulep Foo.Barwould be accessible directly via:"Elixirp.Foo.Bar", which could also be stored in a variable and passed around. -
Modules must be explicitly required+aliased but it warns instead of erroring if you break its boundaries.
Rejected ideas
The following ideas were rejected:
- Declaring the module visibility per package or application. The Elixir language and the compiler do not have the concept of “applications”. Applications and packages are purely a build tool construct. In a way this is great, because the language is small and we build features on top, but it also means we cannot implement a construct such as visibility per package as part of the language.
Proposals
With this in mind, we have four proposals (A, B, C and D). Please criticize those options and your rationale over them. Why you like some and why you dislike others.
They are:
A. Provide @module_visible_to annotations with best-effort warnings
B. Provide defmodulep where modules must be explicitly required+aliased and it will error if you break its boundaries. The namespace the module will be assigned to is private, which means you have no official ways of accessing a private module beyond its original intent.
C. Provide defmodulep where modules must be explicitly required+aliased and it will error if you break its boundaries. However, the namespace the module will be assigned to is public, which means you can access it directly, without any visibility check, by using its long name. For example, defmodulep Foo.Bar would be accessible directly via :"Elixirp.Foo.Bar".
D. Provide defmodulep where modules must be explicitly required+aliased but it warns instead of erroring if you break its boundaries.
If you can think of other implementations and approaches, please drop a comment to so we can amend the proposal accordingly.
Thank you!
Trending in News
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ai
- #ecto-query
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #metaprogramming
- #hex











Showing Posts 157 to 148- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
josevalim
Folks, I will go ahead and close the thread, as it has reached its natural course. Of course, new discussions around the topic are welcome and can link back to it. Thanks!
sasajuric
The
.character is just another character in the module name, so the runtime doesn’t care about it at all. Of course, the tooling can introduce additional rules and conventions (if I’m not mistaken, this is already done with protocols).It doesn’t look like the proposal A introduces any special relationship based on the
.character (or any other character), but it does introduce a special relationship based on the module attribute (@module_visible_to, or however it’s going to be called).I personally like this proposal, since I was never really convinced that
@moduledoc falseis sufficient. This proposal looks lightweight, and it should help with a better communication of intent and make things more enforceable.sergio
@sasajuric How does this mesh with what you said in your book that module name nesting is just syntactic sugar and doesn’t imply any special relationship between modules.
This proposal seems like it will trigger special relations between nested modules. Curious what your thoughts are.
AndrewDryga
We work on a biggish Elixir app and this is something we spent few hours discussing, I believe that private modules is a must have and lean towards option D and C.
I don’t mind would be that error or a warning as we are able to return error on warnings (and we do it already), but with option D debugging would be harder, because you need to know that your function is magically lives in another module
:"Elixirp.Foo.Bar".Also, are private modules going to be accessible from tests unlike private functions? A private module can contain a lot of functionality and testing all edge cases in a high-level function that calls a private module would lead to code duplication and increased complexity. So I beleive we should test it as any other module.
One other thing I’m not sure is listing each caller module individually, namespacing looks more practical here - I would like to allow all modules in current domain context to call private module. But I’m not sure it’s possible to implement it this way because we don’t know all the modules within a context beforehand.
binaryseed
It looks like for the
absinthecase, it was more complex than just using a private APIhttps://github.com/absinthe-graphql/absinthe/commit/96058883c8a67382c0eea41a464136318d9f9717
LostKobrakai
I’ve a few other examples as well:
Afaik @kip was aware he was using private API, but the issue is that there’s no proper incentive to speak to the maintainer to make it public vs. just using the private api as is – at least not yet. Private API in elixir (e.g. mix) is especially problematic because of the rather fixed release cycle. The goal of this proposal is to my understanding to force (or at least promote) the work/discussion of such cases to happen at an earlier stage – at the place where someone want’s to use a private api opposed to when the dependency (elixir, plug, ecto, …) finally updates and breaks things.
Even if the private API could just be made public a few month down the line a package could at least depend on the exact version, where it’s still private, call the private API and make a note to release a new version using any updated public API when it’s released. Anybody updating before the new release would get a warning that the package does not work with e.g. elixir 1.X yet. Not perfect, as the private API could still break before the next release, but at least both parties are aware of the issues and when it’s to be properly resolved.
NobbZ
The problem with
@moduledoc falseis, that one not only needs to see it, but also needs to understand it.Does the one who calls into that module even know the difference (which only is by convention) between no
@moduledoc/@docat all and@moduledoc false/@doc false?axelson
Here’s a few examples:
String.Casing: Don't use String.Casing it breaks in Elixir 1.6 · Issue #5 · blackode/elixir-tips · GitHubMix.Ectowas private in Ecto 2.x. There’s a bunch of blog posts that broke with Ecto 3.x because of itIEx.Autocomplete: How to get code completion in IEx programmatically?I think that you may be overestimating the number of people that will recognize a
@moduledoc falseattribute means that the module is private to the application. Also don’t forget that it is easy for a developer to search on Google, find some example code (that may be using a private module), and then test the code locally and everything works just fine without any warnings. There’s also at least 2 more blog posts that usedMix.Ectowhen it was private, but I couldn’t find them again with a quick search.Eiji
@moduledoc falseis used mostly at start of module. I did not saw it in middle or at bottom, but of course it does not mean nobody did that. Anyway@moduledocas well as@docand@typedocallows to generate documentation which is available at hexdocs.pm. Using everything else than those modules/functions which are placed in this site is seen as bad and unsafe practice, because such internal API could be changed basically in any commit. If you seemodule.function/arityin docs this means it will stay and works same until new major version would be released. From this it’s just not possible to miss@moduledoc falseas developers should not even take a look at internal modules in order to use them in their application.It was always about using
module.function/aritywhich documentation is not published.binaryseed
I’m wondering more about the actual situations, not just an example.
As in, did people know they were reaching into private modules and just really “needed” the function? Or was it accidental, they just didn’t notice the
@moduledoc falseannotation in a thousand line file?Has it always been an attempt to access a private module function, or was it wound up in the details of how something more complex worked, like a macro doing something wild, or the behavior of some kind of module attribute..