Are explicit exports and local imports desirable in a production application?

To clarify, an explicit export:

module X (…) where …

An explicit import:

import X (…)

I would like to question the desirability of explicit exports and local imports in a production application.

I want to make two cases:

  • That explicit exports are never necessary. Moreover, that explicit exports are harmful as they make checking of non-exported definitions impossible.
  • That explicit imports are not necessary when importing a module from the same package.

By default, I make an easy claim that none are desirable since they incur mindless typing effort and thereby unfairly tax the programmer. Let us then consider the justifications for their use despite that.

Explicit exports.

When there are no explicit exports, everything is exported from a module. Recall a case when this is disadvantageous: abstract types. The values of abstract types must only be obtained via smart constructors. So their definitions should not be made available. Explicit exports may provide that.

However, there is another practice: putting dangerous definitions of the supposedly abstract type X into a module X.Internal, then importing them to module X and defining smart constructors there. By default, a module only exports the definitions defined in itself — there are no re-exports. So, a user importing X cannot invoke unsafe constructors from X.Internal, and they know better than to import the dangerous module directly.

In the former case above, the internal definitions remain out of reach for a test suite. In the latter case, they may be freely checked as needed.

Explicit imports.

Recall the scenario in which explicit imports are useful.

  • A module "p" X from package p imports a module "q" Y from package q v0.0.0.
  • The package q bumps the third version number and exports a definition Y.d which name coincides with an already defined definition X.d.

What happens?

  • If imports are explicit, nothing happens. The maintainers of p are safe if they set a restriction as weak as q ^>= 0.0.

  • If imports are implicit, there would be a clash of names if p is built against q v0.0.1, but the build would pass if it is built against a less recent version q v0.0.0.1.

    The maintainers of p might not even notice that the package does not build in some cases. When they do, they would have to set a restriction q ^>= 0.0.0

But surely there is only one version to build against if the two modules reside in the same package. So the overlapping names will necessarily result in an error that would immediately be rectified. It is no different from any other compile time error.

Are my considerations fair?

2 Likes

I think another big advantage of explicit imports is that it makes it easier (especially for beginners) to see where each function comes from.

And explicit exports have two more advantages. You can reorder definitions for the haddock documentation and create haddock sections in the export list. And you can export modules with explicit exports.

3 Likes

I do not like explicit imports, in my opinion qualified modules as

import qualified Data.List as L

do the job better as:

  • you do not need to rework the import line if you start/stop using a simple (and you save time and haveclearer commit diffs);
  • with sensible aliases (L for Data.List, but even Lst etc.), whoever reads your code will get an immediate pointer to the right module where the symbol comes from.

For exports: I find explicit exports invaluable for the reasons highlighted by @jaror.

1 Like

It’s really difficult (and annoying) to read code that does not have explicit imports if you do not have an IDE running (or at least an editor). For example, reading the source code of a library in hackage or github to find out what it does or how it works. I have no idea where loads of functions come from. It’s not a good solution to qualify every single thing with X.function or whatever. It’s so easy to add explicit imports, especially with a good editor or ide that does all this for you. As always, think of the person who is going to read your code.

4 Likes

For imports : I’ve settled (both at work and not) for explicit imports by default and qualified imports with short mnemonics where the namespaces clash (e.g. for container types: fromList, singleton etc.).

For exports : it changes during the evolution of the codebase. I start with no explicit export list and selectively export names as the API emerges from the raw medium. There are internal functions you simply don’t need to test, since they are used by API functions.

As ever, “it depends”.