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

I apologize if this topic has already been clarified elsewhere — please refer me. The present message can also be seen _(and answered)_ on the Haskell Discourse board.[1] 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? [1]: https://discourse.haskell.org/t/are-explicit-exports-and-local-imports-desir...

On Wed, 16 Sep 2020, Ignat Insarov wrote:
### 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.
You can do the following in Cabal: Library private Exposed-Modules: A.Internal Library Exposed-Modules: A Build-Depends: private Test-Suite foobar-test Build-Depends: private This way you can export the constructors of a datatype from A.Internal and use them in A and in the test-suite, but the user of your package cannot access them.
### 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`.
I would not distinguish between imports between modules from the same and from other packages, because in the course of refactoring you might want to split a package. Instead I write all my modules with qualified imports in mind, such that I can write e.g. Window.open instead of openWindow. E.g. 'containers' is a good pattern to follow.

Thank you Henning. So, the notion of internal libraries amounts to an even stronger argument in favour of eschewing explicit exports, at least in the case we consider. Am I reading your implication correctly? As for qualified imports, I also follow this practice and I think it is most readable, given that the qualifications are words and not just letters — alas, using single letter qualifications, such as `T.pack` for `Data.Text.pack`, is common.

On Wed, 16 Sep 2020, Ignat Insarov wrote:
So, the notion of internal libraries amounts to an even stronger argument in favour of eschewing explicit exports, at least in the case we consider. Am I reading your implication correctly?
You need explicit exports in the public interface, in my example module "A".
As for qualified imports, I also follow this practice and I think it is most readable, given that the qualifications are words and not just letters — alas, using single letter qualifications, such as `T.pack` for `Data.Text.pack`, is common.
Yes, I prefer Text.pack and Map.union etc.

You need explicit exports in the public interface, in my example module "A".
How so? Consider an example [1]. Building it yields an error, showing that any unsafe definitions contained in `A.Internal` are out of reach from another package. I must be missing your point. [1]: https://github.com/kindaro/exports-and-internal-libraries

On Wed, 16 Sep 2020, Ignat Insarov wrote:
You need explicit exports in the public interface, in my example module "A".
How so?
My example should be like so: module A.Internal where data T = Private Integer module A ( T, -- do not export constructor f, ) where import A.Internal (T(Private)) f :: FooBar f = do something with Private You would need the export list in A for exposing T, but not Private.

Thank you Henning. This is true. When one wishes to provide an abstract type, surely one would like to re-export the type constructor, but not the associated data constructors — so explicit exports become unavoidable. I have not accounted for this detail.
You need explicit exports in the public interface, in my example module "A".
How so?
My example should be like so:
module A.Internal where
data T = Private Integer
module A ( T, -- do not export constructor f, ) where
import A.Internal (T(Private))
f :: FooBar f = do something with Private
You would need the export list in A for exposing T, but not Private.

* That explicit exports are never necessary. Moreover, that explicit exports are harmful as they make checking of non-exported definitions impossible.
It's not just the internal API in the form of constructors that's worth hiding. What about helper functions? The ones that would be "private" in an OOP language? Should I rip my logical structure apart just because exports are tedious? In fact the OOP separation is a decent framework to reference for perspective: public → (explicitly) exported private → not exported at all package protected → exported from internal module protected → not directly controlled by exports, but by types and available construction paths All have an equivalent, all are necessary. But I agree that explicit exports are flawed, because the vast majority of "stuff" is usually exported. A better way might be to export everything except explicitly hidden stuff. That's not possible right now (I think), but imagine syntax like this: module Foo (..) hiding ( bar, baz ) where or module Foo (module Foo hiding ( bar, baz )) where

On Wed, 16 Sep 2020, MarLinn wrote:
But I agree that explicit exports are flawed, because the vast majority of "stuff" is usually exported. A better way might be to export everything except explicitly hidden stuff. That's not possible right now (I think), but imagine syntax like this:
module Foo (..) hiding ( bar, baz ) where
or
module Foo (module Foo hiding ( bar, baz )) where
In Oberon you do not specify an export list, instead you mark exported identifiers with a star. This way you avoid duplication of identifiers in the export list and the identifier order is always the order of definitions in the module.
participants (3)
-
Henning Thielemann
-
Ignat Insarov
-
MarLinn