Module reexports at the package level

Hi, not sure if this is the right mailing list, as it affects GHC and Cabal, but can be used by all library authors. But it is a close fit. I’d like to propose Module re-reports on the package level, in order to make package reorganization easier on the users. For details, please see http://ghc.haskell.org/trac/ghc/wiki/ModuleReexports and for discussion, I suggest to use the trac ticket http://ghc.haskell.org/trac/ghc/ticket/8407 Questions are: * Is this useful enough? * Is the design (syntax and semantics) good? Thanks, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • http://www.joachim-breitner.de/ Jabber: nomeata@joachim-breitner.de • GPG-Key: 0x4743206C Debian Developer: nomeata@debian.org

* Joachim Breitner
Hi,
not sure if this is the right mailing list, as it affects GHC and Cabal, but can be used by all library authors. But it is a close fit.
I’d like to propose Module re-reports on the package level, in order to make package reorganization easier on the users. For details, please see http://ghc.haskell.org/trac/ghc/wiki/ModuleReexports and for discussion, I suggest to use the trac ticket http://ghc.haskell.org/trac/ghc/ticket/8407
Questions are: * Is this useful enough? * Is the design (syntax and semantics) good?
This looks useful — it could help solve the problem of fine-grained packages, by providing a means to create meta-packages on which one can depend. For that it would be also useful to be able to re-export whole packages, similarly to how we can re-export modules from other modules. I have two concerns about versioning: 1. Suppose that package-a re-exports Data.Bar from package-b starting from package-a-2.0 and package-b-2.0. This means that we shouldn't prevent packages to be built with package-a-1.0 and package-b-2.0, because that would result in a duplicate module Data.Bar. Note that we cannot simply make package-b-2.0 depend on package-a >=2.0, because that would result in a recursive dependency. Yet it might be useful to tell the dependency solver that these packages are incompatible. 2. If package-b has a major API change, it would bump its version, say, to 3.0. package-a still has version 2.0, but now it re-exports a completely different API. Roman

Hi, Am Freitag, den 04.10.2013, 13:05 +0300 schrieb Roman Cheplyaka:
I have two concerns about versioning:
1. Suppose that package-a re-exports Data.Bar from package-b starting from package-a-2.0 and package-b-2.0. This means that we shouldn't prevent packages to be built with package-a-1.0 and package-b-2.0, because that would result in a duplicate module Data.Bar.
you mean „should“, right? I’m not sure if I understand your example. Does package-a-1.0 provide Data.Bar? Are you sure that this is not covered by the PVP?
2. If package-b has a major API change, it would bump its version, say, to 3.0. package-a still has version 2.0, but now it re-exports a completely different API.
Good point. But package-a depends on package-b, and hence a major API bump in package-b will require at least a new minor version of packaga-a. It is then the obligation of the author of package-a to check if the API changes affect the re-exported packages (and I hope that eventually we’ll have tools that help with that). Greetings, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • http://www.joachim-breitner.de/ Jabber: nomeata@joachim-breitner.de • GPG-Key: 0x4743206C Debian Developer: nomeata@debian.org

* Joachim Breitner
Hi,
Am Freitag, den 04.10.2013, 13:05 +0300 schrieb Roman Cheplyaka:
I have two concerns about versioning:
1. Suppose that package-a re-exports Data.Bar from package-b starting from package-a-2.0 and package-b-2.0. This means that we shouldn't prevent packages to be built with package-a-1.0 and package-b-2.0, because that would result in a duplicate module Data.Bar.
you mean „should“, right?
Yes, sorry.
I’m not sure if I understand your example. Does package-a-1.0 provide Data.Bar? Are you sure that this is not covered by the PVP?
The problematic situation could look like this: package-a-1.0 provides Data.Bar package-b-2.0 provides Data.Bar package-a-1.0.1 re-exports Data.Bar from package-b-2.0 Now, the change between package-a-1.0 and package-a-1.0.1 may seem almost invisible for the users, but if something depends on package-a ==1.0.*, package-b ==2.0 ... then we have a conflict in case package-a-1.0 is picked. But now I think this example is contrived because the constraint should really be package-a >= 1.0.1 && < 1.1 (we could only test our package successfully with package-a-1.0.1, and even ignoring module re-exports, package-a-1.0.1 could have added new entities).
2. If package-b has a major API change, it would bump its version, say, to 3.0. package-a still has version 2.0, but now it re-exports a completely different API.
Good point. But package-a depends on package-b, and hence a major API bump in package-b will require at least a new minor version of packaga-a. It is then the obligation of the author of package-a to check if the API changes affect the re-exported packages (and I hope that eventually we’ll have tools that help with that).
You are right again — I missed the fact that dependency package-a -> package-b itself has a version constraint. Roman

Hi, Am Freitag, den 04.10.2013, 15:43 +0300 schrieb Roman Cheplyaka:
The problematic situation could look like this:
package-a-1.0
provides Data.Bar
package-b-2.0
provides Data.Bar
package-a-1.0.1
re-exports Data.Bar from package-b-2.0
Now, the change between package-a-1.0 and package-a-1.0.1 may seem almost invisible for the users, but if something depends on
package-a ==1.0.*, package-b ==2.0
... then we have a conflict in case package-a-1.0 is picked.
But now I think this example is contrived because the constraint should really be package-a >= 1.0.1 && < 1.1 (we could only test our package successfully with package-a-1.0.1, and even ignoring module re-exports, package-a-1.0.1 could have added new entities).
well, its a good observation: Changing a module from existing to re-exporting requires a bump at least in the third position (1.0.1), not just the last (1.0.0.1), as if you are adding a new module, as people can move from 1.0 to 1.0.1, but not necessarily the other way around. Greetings, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • http://www.joachim-breitner.de/ Jabber: nomeata@joachim-breitner.de • GPG-Key: 0x4743206C Debian Developer: nomeata@debian.org

On 04/10/2013 11:07, Joachim Breitner wrote:
not sure if this is the right mailing list, as it affects GHC and Cabal, but can be used by all library authors. But it is a close fit.
I’d like to propose Module re-reports on the package level, in order to make package reorganization easier on the users. For details, please see http://ghc.haskell.org/trac/ghc/wiki/ModuleReexports and for discussion, I suggest to use the trac ticket http://ghc.haskell.org/trac/ghc/ticket/8407
Questions are: * Is this useful enough? * Is the design (syntax and semantics) good?
It's not clear whether it's worth doing this, because it doesn't add any new functionality, only convenience. To re-export a module from a package you can already write a stub module like this: {-# LANGUAGE PackageImports #-} module Data.Foo (module X) where import "foo" Data.Foo as X so the convenience would be not having to write (or automatically generate) all these stub modules. You could implement package re-exports by automatically generating these stub modules, which would require only changes in Cabal. Or you could implement them directly, which would need changes in lots of places - when I considered doing this before, I concluded that it probably wasn't worth it. Cheers, Simon

There is one element beyond convenience.
If you launch GHCi, and load Data.Foo, then if we follow the PackageImports
path there will be two modules with the same name and the choice of where
to load it from will be flagged as ambiguous, asking if you meant Data.Foo
from "foo" or Data.Foo from "bar", no?
-Edward
On Mon, Oct 14, 2013 at 11:39 AM, Simon Marlow
On 04/10/2013 11:07, Joachim Breitner wrote:
not sure if this is the right mailing list, as it affects GHC and Cabal, but can be used by all library authors. But it is a close fit.
I’d like to propose Module re-reports on the package level, in order to make package reorganization easier on the users. For details, please see http://ghc.haskell.org/trac/**ghc/wiki/ModuleReexportshttp://ghc.haskell.org/trac/ghc/wiki/ModuleReexports and for discussion, I suggest to use the trac ticket http://ghc.haskell.org/trac/**ghc/ticket/8407http://ghc.haskell.org/trac/ghc/ticket/8407
Questions are: * Is this useful enough? * Is the design (syntax and semantics) good?
It's not clear whether it's worth doing this, because it doesn't add any new functionality, only convenience. To re-export a module from a package you can already write a stub module like this:
{-# LANGUAGE PackageImports #-} module Data.Foo (module X) where import "foo" Data.Foo as X
so the convenience would be not having to write (or automatically generate) all these stub modules.
You could implement package re-exports by automatically generating these stub modules, which would require only changes in Cabal. Or you could implement them directly, which would need changes in lots of places - when I considered doing this before, I concluded that it probably wasn't worth it.
Cheers, Simon ______________________________**_________________ Libraries mailing list Libraries@haskell.org http://www.haskell.org/**mailman/listinfo/librarieshttp://www.haskell.org/mailman/listinfo/libraries

Hi, Am Montag, den 14.10.2013, 17:39 +0200 schrieb Simon Marlow:
On 04/10/2013 11:07, Joachim Breitner wrote:
not sure if this is the right mailing list, as it affects GHC and Cabal, but can be used by all library authors. But it is a close fit.
I’d like to propose Module re-reports on the package level, in order to make package reorganization easier on the users. For details, please see http://ghc.haskell.org/trac/ghc/wiki/ModuleReexports and for discussion, I suggest to use the trac ticket http://ghc.haskell.org/trac/ghc/ticket/8407
Questions are: * Is this useful enough? * Is the design (syntax and semantics) good?
It's not clear whether it's worth doing this, because it doesn't add any new functionality, only convenience. To re-export a module from a package you can already write a stub module like this:
{-# LANGUAGE PackageImports #-} module Data.Foo (module X) where import "foo" Data.Foo as X
so the convenience would be not having to write (or automatically generate) all these stub modules.
thanks for looking at this. You are missing an important bit of semantics in my proposal: Current, if two packages foo and bar are visible, both of which provide Data.Foo, you cannot use "import Data.Foo" – even if one is a re-exporting stub as you suggest. With what I have in mind, if foo re-exports bar’s Data.Foo, such an import would be accepted. I believe this is important, as it allows package restructuring with less breaking existing code. There is an alternative solution to this: Use re-exporting stub modules (as you suggest), but make GHC more liberal when multiple modules of the same name are important: E.g. * ignore one if it exports precisely the same set of names and values, or * if it exports a subset, or even * only complain if an ambiguous _name_ is used from the module, and do not complain if a name is used that is exported only by one Data.Foo, or is exported by both, but referring to the same symbol. These have their merits (more powerful), but can also be abused more, so but I prefer the original, less ad-hoc and more declarative approach. Greetings, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • http://www.joachim-breitner.de/ Jabber: nomeata@joachim-breitner.de • GPG-Key: 0x4743206C Debian Developer: nomeata@debian.org

On 14/10/2013 19:33, Joachim Breitner wrote:
thanks for looking at this. You are missing an important bit of semantics in my proposal: Current, if two packages foo and bar are visible, both of which provide Data.Foo, you cannot use "import Data.Foo" – even if one is a re-exporting stub as you suggest. With what I have in mind, if foo re-exports bar’s Data.Foo, such an import would be accepted. I believe this is important, as it allows package restructuring with less breaking existing code.
Yes, good point. This wasn't an issue when we were using stubs before, because you would be using *either* base-3 or base-4, but not both.
There is an alternative solution to this: Use re-exporting stub modules (as you suggest), but make GHC more liberal when multiple modules of the same name are important: E.g. * ignore one if it exports precisely the same set of names and values, or * if it exports a subset, or even * only complain if an ambiguous _name_ is used from the module, and do not complain if a name is used that is exported only by one Data.Foo, or is exported by both, but referring to the same symbol. These have their merits (more powerful), but can also be abused more, so but I prefer the original, less ad-hoc and more declarative approach.
The third option sounds reasonable, as a generalisation of the way ambiguous identifiers are currently handled. In fact, if we want to re-export only a subset of the original module, then we have to use a stub (rather than re-exporting at the package level), and so we end up needing this change to ambiguity checking. Re-exporting a subset of another module is common. If we're providing an old API in terms of a new one, each of the modules of the old API will re-export the subset of the new API that corresponds to the old API. Cheers, Simon

Hi, Am Dienstag, den 15.10.2013, 09:50 +0200 schrieb Simon Marlow:
There is an alternative solution to this: Use re-exporting stub modules (as you suggest), but make GHC more liberal when multiple modules of the same name are important: E.g. * ignore one if it exports precisely the same set of names and values, or * if it exports a subset, or even * only complain if an ambiguous _name_ is used from the module, and do not complain if a name is used that is exported only by one Data.Foo, or is exported by both, but referring to the same symbol. These have their merits (more powerful), but can also be abused more, so but I prefer the original, less ad-hoc and more declarative approach.
The third option sounds reasonable, as a generalisation of the way ambiguous identifiers are currently handled. In fact, if we want to re-export only a subset of the original module, then we have to use a stub (rather than re-exporting at the package level), and so we end up needing this change to ambiguity checking.
Re-exporting a subset of another module is common. If we're providing an old API in terms of a new one, each of the modules of the old API will re-export the subset of the new API that corresponds to the old API.
you really think so? I was not bold enough to suggest that, but OTOH many nice things can be done with it (and many non-nice). For example, if we’d implement that, it means that packages can start to provide a module named "Prelude" to automatically and conveniently amend the default imports. While it would definitely solve my itch, I’m a bit worried that it would open the floodgates to quite messy cross-package modules. Greetings, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • http://www.joachim-breitner.de/ Jabber: nomeata@joachim-breitner.de • GPG-Key: 0x4743206C Debian Developer: nomeata@debian.org
participants (4)
-
Edward Kmett
-
Joachim Breitner
-
Roman Cheplyaka
-
Simon Marlow