Ticket #4879 (new feature request)

Opened 2 years ago

Last modified 8 months ago

Deprecate exports

Reported by: basvandijk Owned by:
Priority: high Milestone: 7.8.1
Component: Compiler Version: 7.0.1
Keywords: Cc:
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Difficulty: Unknown
Test Case: Blocked By:
Blocking: Related Tickets:

Description

Motivation

During the library submission process there's sometimes the desire to have the ability to deprecate an export from a module.

For example  during the discussing about ticket #4422, I would have liked the ability to deprecate the exports of the String functions: lines, words, unlines and unwords from Data.List in favour of importing them from Data.String. However I wasn't able to do so, so these exports remain.

Similarly,  during the discussion about ticket #4865, Ian also desired to deprecate the export of catch from System.IO.Error but was unable to do so.

Syntax

To deprecate an export simply place a DEPRECATE pragma for the export inside the export list, as in:

module Data.List
  (  ...
  {-# DEPRECATE lines "Exported from Data.String instead" #-}
  , lines
  ...
  ) where
...

Another design might be to have a different pragma as in:

{-# DEPRECATE_EXPORT lines "Exported from Data.String instead" #-}

But I find the former much prettier and more obvious.

Semantics

If the lines export from Data.List is deprecated the following should raise deprecation warnings:

  • Directly importing a deprecated export:
    import Data.List (lines)
    
  • Referring to a deprecated export:
    import Data.List
    foo = lines
    

If you import the same symbol from different modules and only some of them are deprecated exports then referring to the symbol won't give a deprecation warning. For example the following should not give deprecation warnings:

import Data.List
import Data.String
foo = lines

What exports can be deprecated?

  • Functions.
  • Types.
  • Classes.
  • Constructors. Possible syntax:
    module A
      ( {-# DEPRECATE T(C1) "The export of C1 is deprecated" #-}
        T(C1, C2, C3)
      ) where
    
  • Modules. Possible syntax:
    module A
      ( {-# DEPRECATE module B "The export of module B is deprecated" #-}
        module B
      ) where
    

The semantics of deprecating a module export is that you get deprecation warnings for all symbols from module B that you refer to inside a module that imports A. (Does that make sense?)

Change History

follow-up: ↓ 3   Changed 2 years ago by simonpj

How does this differ from what we have now? You can say

  {-# DEPRECATE lines "Exported from Data.String instead" #-}

in Data.List and it will work just as you say. Maybe you can elaborate your proposal to say how it relates the current mechanisms?

  Changed 2 years ago by igloo

You can't do this:

module Q (foo) where

foo :: ()
foo = ()
module W (foo) where

import Q

{-# DEPRECATED foo "Some reason" #-}
$ ghc --make W.hs
[1 of 2] Compiling Q                ( Q.hs, Q.o )
[2 of 2] Compiling W                ( W.hs, W.o )

W.hs:6:16:
    The deprecation for `foo' lacks an accompanying binding
      (You cannot give a deprecation for an imported value)

in reply to: ↑ 1   Changed 2 years ago by basvandijk

Replying to simonpj:

How does this differ from what we have now? You can say {{{ {-# DEPRECATE lines "Exported from Data.String instead" #-} }}} in Data.List and it will work just as you say. Maybe you can elaborate your proposal to say how it relates the current mechanisms?

The current mechanism allows you to deprecate a definition. I would like to deprecate an export. In the case of lines, I would like to move the definition of lines to Data.String and re-export it from Data.List so that user code doesn't break immediately.

However, I do want to mark the export of lines from Data.List as deprecated so that users are warned that in the next version of base they need to import it from Data.String:

module Data.String 
  ( ...
  , lines
    ... 
  ) where

lines = ...
module Data.List 
  ( ...
    {-# DEPRECATE lines "Exported from Data.String instead" #-}
  , lines
    ... 
  ) where

import Data.String ( lines )

We could keep the definition of lines in Data.List and deprecate it and define a new function lines in Data.String. However this has the disadvantage that users importing both Data.List and Data.String get an "ambiguous occurrence of lines" error unless they use qualified imports.

In the proposed mechanism we don't have this problem and there's no need for a duplicate definition.

  Changed 2 years ago by igloo

  • milestone set to 7.2.1

follow-up: ↓ 6   Changed 2 years ago by simonpj

Ah, now I see. It's a bit like deprecating the "definition" of lines in Data.List, where in this case the "definition" is the import from Data.String.

But there are questions of course. What if I now say

module Foo( lines ) where
  import Data.List( lines )

Do I get a deprecation message from the mention of lines in the export list? What if there was no export list was moudle Foo( module Data.List )?

What if something is in scope more than one way, one deprecated and one not:

module Bar where
  import Data.List( lines )
  import Data.String( lines )
  f = lines

Do I get a deprecation warning? Or does the use "pick the best" import declaration?

What if something is imported two ways, and both are separately deprectated. Does it accumulate two deprecation warnings?

What if a moudule M imports Data.List(lines), and re-exports lines? Is an import from M deprecated? That is, are deprecations transitive?

Thinking about it, a possible view is this:

  • The existing deprecating mechanism attaches a deprecation to a definition, and complains at a use, but not at imports.
  • The new mechanism attaches a deprecation to an export (from module M, say), and complains at import (of module M only), but but not at uses.

Under this interpretation it's not clear whether plain import module M should do.

But regardless, there are lots of details to be worked out. Whether the pain is worth the gain is not clear to me.

Simon

in reply to: ↑ 5   Changed 2 years ago by simonmar

We have another use for this: since Haskell 2010, Foreign.unsafePerformIO is now deprecated, you are supposed to get it from System.IO.Unsafe.unsafePerformIO. I didn't want to make it a distinct identifier, because that would cause name clashes, but I would like to deprecate uses of Foreign.unsafePerformIO.

In other words, I would like a warning to be emitted iff a compilation error would ensue if the deprecated export was removed.

So, if the user imports Foreign.unsafePerformIO by name, the warning would be generated.

If unsafePerformIO is in scope via two routes and is used somehow, that's ok - no warning needs to be generated. If it was in scope via multiple routes and all routes were deprecated, then I expect we should emit all the warnings.

  Changed 16 months ago by igloo

  • priority changed from normal to low
  • milestone changed from 7.4.1 to 7.6.1

  Changed 10 months ago by simonmar

  • priority changed from low to high
  • difficulty set to Unknown

  Changed 8 months ago by igloo

  • milestone changed from 7.6.1 to 7.8.1
Note: See TracTickets for help on using tickets.