Proposal: Private modules (general discussion)

Exactly my point. I am little uncomfortable with us having to compose the exact atom module name in prod iex settings. Having a convenience escape-hatch function/macro loses us nothing and, again, clearly communicates your intent. It can also help when testing such modules (although testing implementation details should be a no-go by default).

As I said above though, I would not insist on it. Just a small quality-of-life improvement, nothing major.

In that case, theyā€™ll be unable to access it from any other module anyway.

But I see your point. It certainly helps to keep the private specification closer together.

Perhaps we could make it an iex helper? That way we hide the implementation detail while also not exposing the function to everyone.

Having it an iex helper would even make name mangling possible again.

2 Likes

In my first proposal :visible_to was a required attribute but the proof of concept has evolved and made it optional. So I believe it can be made an attribute to. We can discuss this with more details in a later step when (or if) we pick one of those proposals.

Yup, that wouldnā€™t hurt either.

Eh, you can modify the visible_to: [...] part anytime you wish.

IMO any new language featuresā€™ UX must be micro-optimized to hell for minimum possible mistakes when using them.

1 Like

So, these solutions are primarily targeted at people who are using private APIs and are unaware that they are doing so. Is that common? If the predominant case is that people are using private APIs and they are aware, does this solve anything?

I donā€™t have a good gauge on that.

EDIT: I guess one benefit I can think of is explicitness over convention

According to Jose in both threads so far, people are left with the impression that Elixir itself breaks backwards compatibility ā€“ when clearly people try to force their way in to quickly solving a problem and using an API that is intended to be private in the process. This is undesirable for marketing reasons and it doesnā€™t matter if people were doing things wrong if they step away from Elixir with the wrong impressions and never give it another chance. This is made worse if they advise friends and colleagues.


As for legitimate uses, Iā€™d use this mechanism to communicate intent that certain modules are implementation details and that only their direct user modules ā€“ who provide better public API ā€“ must be able to access them. Having to deal with ugly and malformed external data (3rd party APIs give you those all the time) is a very good candidate for a private module. Ecto schema modules when dealing with a big and convoluted legacy database ā€“ another.

Introducing such a feature is a clear win in both cases: when people unwittingly break contracts, and when they want to introduce semantic boundaries in their projects.

5 Likes

Since private modules are deemed a necessary addition, I think option D is the best compromise

  • Warnings instead of hard errors
  • The require+alias macro makes the feature perhaps more discoverable and more explicit than A

I guess I donā€™t see how any of these solve that problem. My guess is most people indirectly use private APIs indirectly via a library. So, when they upgrade Elixir and that library breaks, the assumption is Elixir broke it. If people are directly using private APIs and not realizing that theyā€™re doing so, I could see what youā€™re saying being true.

It is a combination of everything said.

Many developers donā€™t think about boundaries because the language has no constructs to say so (besides @moduledoc false and @doc false, which are minimal). This means people donā€™t write annotations, which means that even if a developer would respect those boundaries, the boundaries are not there. And similarly, because those boundaries are not easy to check, you may accidentally cross them, even when they are defined.

For example, imagine you want to provide a reduce_with_super_power. You may do so by copying Enum.reduce source and then changing it a bit. The problem is that Enum.reduce called a private module, but you didnā€™t bother to check everything the code was calling when you copied that small snippet, and now your code may break in the next Elixir release.

This is just one example. Working on Ecto, Plug and Phoenix I saw it happening in other situations too. However, if someone knows that something is private and they decide to call it anyway, then there is nothing we can do. But at least they have been told so.

In general, it is about intent and communication and it is really hard to show intent and communicate when you have limited constructs to do so.

8 Likes

I have some complicated formulas (ex: related to daily items average pricing calculation, weekly automatic discount placement, etc) in some modules that are only used by 1 or 2 modules. One These complicated formulas need to be splitted in different modules (for readability and testability), probably 3 to 5, one of them has 8 modules. Do I need to make them private?

Would hiding them be beneficial to my code? I havenā€™t tried it. But for sure, if I hide those modules, I only have to provide the public modules to be visible for other components to use. So this is much like the context principle in Phoenix? Access some functionalities only from its boundary? :thinking: I kinda think that this private modules (if implemented) will push us to write smaller modules in the future.

Would this become confusion to my other components as well? Because I already have some public and private functions in some modules. But surely, this will change how I test my Elixir code if want to utilize private modules.

Does anybody have an example of private modules in other languages? Especially functional ones.

Maybe we could learn from them.

Yup! I am interested in exploring the impact this feature may have in the language in terms of code organization and documentation. As @imetallica said, defmodulep really isnā€™t a good name, so maybe we can find a name that is more intention revealing and will guide us in the matters of code organization.

One of my friend said he has 18 modules that have only one top function to call in order to call 18 of them all. It was a huge giant module that was later refactored into 18 (soon to be 20) small modules. He said that each of those 18 modules have its own test file and live inside our CI/CD pipeline.

We still donā€™t know if those 18 modules should be put under private (if module private is going to be implemented), and we still have no idea on how to test them once they gone private.

If you are willing to experiment, you can give it a try now using the POC from the previous discussion: https://github.com/josevalim/defmodulep

The README also talks about testing but it should be mostly transparent. You can also bypass visibility for testing by using the qualified name. It is all in the README. Please do report your findings!

3 Likes

I can not tell about much, but the last time I checked the issue in haskell (which has proper namespaced modules), it was common to have an INTERNAL-namespace directly beneath the main namespace of the library.

There was only this convention, but much more obvious and discoverable than to have @moduledoc false. Especially when reading foreign code its more obvious that a certain piece is meant to be private.

I have no clue how it is done in other functional languages.

Another system Iā€™m aware of, not in a functional language though, is gos. There you name a package internal (IIRC) and all its childs will only be accessible by childs of its parent. Due to the way how gos packages are namespaced (the source repository is part of the name) you canā€™t even mimick to be in that package. (Well, one could with abusing dependency vendoring, and then youā€™ll know when you update anyway).

In Java and C++ namespaces/packages can not be hidden, only their content. And its easy to open up even foreign namespaces and ā€œimpersonateā€ to get access.

So to be honest, I have not yet seen a system that realy makes privte stuff private, but Iā€™d prefer an explicit ā€œI am privat, you shall not use meā€ over a ā€œIā€™m having no documentation, so please pretend you havenā€™t seen meā€.

3 Likes

One thing that is conspicuously lacking from this entire thread is any discussion of how private modules might be tested.

As @dimitarvp alluded to, there is often a reflex to say ā€œdonā€™t test implementation detailsā€, but when decomposing complicated functionality among several smaller private functions Iā€™ve often regretted not being able to test them. I can easily imagine that Iā€™d want to test the code in a private module as much if not more.

How would testing relate to the four proposals?

Edit: Or at least conspicuously lacking until the last few minutes while I was writing this! Still, I would like to know how testing would work in each of the four proposals.

3 Likes

You still can test them when they are private, just make sure that your tests have the correct alias-prefix.

Also similar to the proposed iex helper, some ex_unit helper would be thinkable as well, but Iā€™m not sure if that is really necessary.

Last but not least, is this a library or a standalone application? If its the latter, hiding the modules might not be necessary at all. If it is a library though, Iā€™d hide the modules by either @moduledoc false or the proposed defmodulep.

I think this can very quickly turn into a game of whack-a-mole. First iex, then ex_unit, then who knows what else In-The-FutureĀ®.

Wouldnā€™t having a function like Module.get_private/1 that knows exactly how to find and return the private module be a better option?

1 Like

I do not think so. But perhaps a :"Elixirp.Module.Private".get_private/1 which is declared in a way that iex and ex_unit only need to prepare wrappers? Then it wonā€™t get out of sync if mangling or other details change.

If we expose that function directly, nothing (or not much at least) would change from where we are now, as it is still to easy to circumvent.

But of course, in ex_unit it should be never necessary, as you can easily create the test-module in a namespace that has access to the hidden/private module.

1 Like