Deprecation being transitively reported for re-exported definitions

Hello Café, TL;DR: should the deprecation GHC option be transitively reported for re-exported definitions? I have a library that is exposing too much. As a minimal example, say the library contains: - Module A, which defines several functions and types. - Module B, which exports *specific definitions* from module A and has none of its own. It so happens that, to keep things as clean and abstract as possible, only module B should be exposed. As per library policy, we give users time to adapt. A way to do that would be to deprecate module A, but configure B to ignore deprecations (-Wno-deprecations) so GHC does not complain during the compilation of the library itself. My expectation was that library users who imported *A directly* would get a warning, but importing definitions in A *via B* would not give out any warnings. That, however, is not what is happening: In the use of ‘functionInA’ (imported from B, but defined in A): Deprecated: "This module will be hidden in future versions." There are "workarounds": I could move all definitions in A to new module C, deprecate A, and re-export C in B, or I could re-define the exported definitions in B as identities of those in A (easy for functions, probably more cumbersome for data constructors or classes.) However, more generally, if you use a function from A in a NEW function definition in B and then export *that second definition instead*, the compiler won't tell *the library user* that B is internally relying on a deprecated function. Reexporting a function without changes could conceptually be seen as an "extreme" case of that, where where the name and the implementation in B coincide with those in A. So I ask: should deprecation work the way it is working in the first place? All the best, Ivan

On 14 Aug 2021, at 9:22 pm, Ivan Perez
wrote: I have a library that is exposing too much. As a minimal example, say the library contains: - Module A, which defines several functions and types. - Module B, which exports specific definitions from module A and has none of its own.
The simplest solution might be: module B (somefunction) import qualified A somefunction = A.somefunction This creates a *new* function in B, which is not deprecated, without re-exporting anything from A. -- Viktor.

Hi
Yes. As I said, "I could re-define the exported definitions in B as
identities of those in A (easy for functions, *probably more cumbersome for
data constructors or classes*)."
For example, if B needs to re-export a record with fields and data
constructors, then you can't export those with a type synonym. You can
re-define and export the data constructor and the record fields as
functions, but then you can't do pattern matching or use record syntax on
them anymore.
More generally, the question remains. Should it work the way it does?
Cheers,
Ivan
On Sat, 14 Aug 2021 at 21:40, Viktor Dukhovni
On 14 Aug 2021, at 9:22 pm, Ivan Perez
wrote: I have a library that is exposing too much. As a minimal example, say the library contains: - Module A, which defines several functions and types. - Module B, which exports specific definitions from module A and has none of its own.
The simplest solution might be:
module B (somefunction)
import qualified A
somefunction = A.somefunction
This creates a *new* function in B, which is not deprecated, without re-exporting anything from A.
-- Viktor.
_______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.

On Sat, Aug 14, 2021 at 09:53:14PM -0400, Ivan Perez wrote:
For example, if B needs to re-export a record with fields and data constructors, then you can't export those with a type synonym. You can re-define and export the data constructor and the record fields as functions, but then you can't do pattern matching or use record syntax on them anymore.
Indeed, it doesn't work for types *in general*.

Ivan mentioned that option in his email—seems like that could be okay (but
awkward!) for functions, but would not work well for other definitions like
typeclasses.
On Sat, Aug 14, 2021, 18:44 Viktor Dukhovni
On 14 Aug 2021, at 9:22 pm, Ivan Perez
wrote: I have a library that is exposing too much. As a minimal example, say the library contains: - Module A, which defines several functions and types. - Module B, which exports specific definitions from module A and has none of its own.
The simplest solution might be:
module B (somefunction)
import qualified A
somefunction = A.somefunction
This creates a *new* function in B, which is not deprecated, without re-exporting anything from A.
-- Viktor.
_______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.

I think the confusion stems from what the deprecation of the module means, i.e. module A {-# DEPRECATED "This module will be hidden in future versions". #-} ( ... ) where I think it does two things: 1. deprecates the module A, so if it's imported anywhere, the deprecation warning will be reported 2. deprecates all symbols defined in the module, so the use-sites are reported as well. (This is like deprecating an individual binding, {-# DEPRECATED symbolInA "..." #-}. The second point is why re-exporting names defined in A still causes the warning. The thing is deprecated, it doesn't matter how you import it. However, if A re-exports some other symbols (e.g. from A.Internal), these things are not deprecated, and thus no warnings. This explains why a workaround you mention works. Or we could even argue that the (first) workaround is not even a workaround, but the right way to do what you want. Defining new binding is not the same as re-exporting. There is a bit discussion about it in [1], e.g. users can define different RULES for the thing "renamed" in B. There is an interesting challenge in [1] too:
the proposal would be stronger if it explicitly explained that much of what is proposed [renaming on import] could be done with existing mechanisms [like writing new definitions, type, etc.]
For your use case (in second workaround) you'll rely that redefinition (as "heavy" renaming) will strip the deprecation bit. But we can argue that it's still a renaming, so it should not! :) [1]: https://github.com/ghc-proposals/ghc-proposals/pull/408#issuecomment-8060648... - Oleg On 15.8.2021 4.22, Ivan Perez wrote:
Hello Café,
TL;DR: should the deprecation GHC option be transitively reported for re-exported definitions?
I have a library that is exposing too much. As a minimal example, say the library contains: - Module A, which defines several functions and types. - Module B, which exports /specific definitions/ from module A and has none of its own.
It so happens that, to keep things as clean and abstract as possible, only module B should be exposed.
As per library policy, we give users time to adapt. A way to do that would be to deprecate module A, but configure B to ignore deprecations (-Wno-deprecations) so GHC does not complain during the compilation of the library itself.
My expectation was that library users who imported /A directly/ would get a warning, but importing definitions in A /via B/ would not give out any warnings.
That, however, is not what is happening:
In the use of ‘functionInA’ (imported from B, but defined in A): Deprecated: "This module will be hidden in future versions."
There are "workarounds": I could move all definitions in A to new module C, deprecate A, and re-export C in B, or I could re-define the exported definitions in B as identities of those in A (easy for functions, probably more cumbersome for data constructors or classes.)
However, more generally, if you use a function from A in a NEW function definition in B and then export /that second definition instead/, the compiler won't tell /the library user/ that B is internally relying on a deprecated function. Reexporting a function without changes could conceptually be seen as an "extreme" case of that, where where the name and the implementation in B coincide with those in A.
So I ask: should deprecation work the way it is working in the first place?
All the best,
Ivan
_______________________________________________ Haskell-Cafe mailing list To (un)subscribe, modify options or view archives go to: http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe Only members subscribed via the mailman list are allowed to post.

On Sat, 14 Aug 2021 at 23:00, Oleg Grenrus
I think the confusion stems from what the deprecation of the module means, i.e.
module A {-# DEPRECATED "This module will be hidden in future versions". #-} ( ... ) where
I think it does two things:
1. deprecates the module A, so if it's imported anywhere, the deprecation warning will be reported 2. deprecates all symbols defined in the module, so the use-sites are reported as well. (This is like deprecating an individual binding, {-# DEPRECATED symbolInA "..." #-}.
This was also my understanding AFTER I saw the unexpected warning. So, you can say that *a definition within a module is deprecated*, and that *a module AND ALL definitions in it are deprecated. *However*, *you cannot express that* the module (name) itself is deprecated without deprecating what's in it*. It's just a level of expressiveness we do not have. If module deprecation did not lead to what you labeled (2), you could still deprecate everything in it to warn users of deprecated functions that are being re-exported through a different path. So you could obtain the same effect as now, by just annotating specific definitions. Ivan
participants (5)
-
Ivan Perez
-
Oleg Grenrus
-
Tikhon Jelvis
-
Tom Ellis
-
Viktor Dukhovni