1 Namespace-specified imports¶
This proposal supports users who wish to write code without relying on punning,
by allowing imports of all names in either the type or data namespace.
Moreover it clarifies the specification of ExplicitNamespaces
and related
extensions.
This topic has been discussed at length previously (see in particular proposal
#214, proposal #236 and proposal #270). The current
proposal draws heavily from this previous work and benefits from efforts to
address these issues by Richard Eisenberg, Artyom Kuznetsov and Vladislav
Zavialov (amongst others). This proposal is an attempt to systematise previous
ideas around ExplicitNamespaces
and namespace-specified imports, primarily
from proposal #270.
In order to keep it narrowly focused, it does not address warnings for the use
of punning (since those are covered by proposal #270) nor changes outside
of import/exports.
1.1 Motivation¶
1.1.1 Background¶
Haskell has two namespaces:
the type namespace, including the names of type constructors, type synonyms, type families and type classes;
the data namespace, including the names of term-level values, functions, data constructors and pattern synonyms.
This separation allows us to define data constructors and type constructors whose names coincide:
data T = T
(Similarly, with the TypeOperators
extension it is possible to define
term-level operators and type families whose names coincide.)
At use sites, GHC infers which T
is referred to from the context:
t :: T -- type-level T
t = T -- term-level T
The use of identical names for type-level and term-level entities is called punning. Haskell makes heavy use of punning in its built-in syntax and common types.
However, there are various contexts in which an occurrence of a name may refer either to the type namespace or the data namespace, and it is not always clear which is meant. In particular, the following may mention both type-level and term-level entities:
Import and export lists
Fixity declarations
WARNING
,DEPRECATED
andANN
pragmasTypes, when using the
DataKinds
extension to reference a data constructor at the type levelTemplate Haskell name quotes
The simplest way to avoid namespace ambiguity is to avoid punning entirely, so
there is no need for the context to determine which namespace is meant.
Proposal #270
introduces warnings -Wpuns
and -Wpun-bindings
to alert users (who opt in
to the warnings) when they are introducing or relying on punning.
However, given the pervasive use of punning in the Haskell ecosystem, even users who wish to avoid punning will inevitably end up importing modules which make use of it. Thus we need mechanisms to disambiguate the namespace on import or at use sites.
1.1.2 Mechanisms for disambiguating puns¶
Various mechanisms for disambiguating punned identifiers exist already:
The
DataKinds
extension introduces the tick syntax ('
) to select entities from the data namespace in a type-level context:p1 :: Proxy T -- Refers to the type constructor T p1 = Proxy p2 :: Proxy 'T -- Refers to the data constructor T p2 = Proxy
TemplateHaskell
name quotes use'
for the data namespace and''
for the type namespace.Proposal #65 allows fixity declarations,
WARNING
andDEPRECATED
pragmas to be modified with avalue
ortype
namespace specifier. (This proposal has been accepted but not yet implemented at the time of writing.)ANN
pragmas refer to the data namespace by default, but may use thetype
keyword to refer to the type namespace.The
ExplicitNamespaces
extension allows thetype
keyword to be used in an import or export list to select the type namespace, typically when usingTypeOperators
to define an operator that would otherwise be imported/exported in the data namespace.The
PatternSynonyms
extension allows thepattern
keyword to be used in an import or export list to select the data namespace, typically when referring to a pattern synonym. (However, it may also refer to a data constructor without its parent type constructor, a form of import/export which is not otherwise possible.)
However, the status quo has some problems:
It is confusing and inconsistent that a prefix
'
has one meaning in terms (TemplateHaskell
name quotes) and a completely different meaning in types (withDataKinds
, use the data namespace rather than the type namespace).The data namespace is referred to by
value
in proposal #65 butpattern
in import/export lists when usingPatternSynonyms
. We should pick a consistent keyword to refer to it, in the interests of simplicity.Users should not be required to enable
PatternSynonyms
and use thepattern
keyword if all they actually want is to refer to a data constructor in an import/export list.The
ExplicitNamespaces
extension allowstype
to be used with a name in the data namespace (if it is unambiguous), see GHC issue #22581.When users wishing to avoid punning are importing modules that define punned entities, they must make careful use of explicit import lists,
ExplicitNamespaces
andPatternSynonyms
to avoid importing the same name into both namespaces. It would be much simpler if they could selectively import “all names in the type namespace” or “all names in the data namespace” (perhaps with different module qualifiers).
1.1.3 Solution Overview¶
To help programmers deal with the external code that uses punning we propose to
extend the ExplicitNamespaces
extension to allow the data
and type
keywords to be used as part of import or export lists, potentially with a ..
wildcard. For example:
import qualified Data.Proxy as T (type ..) -- import only the type namespace
import qualified Data.Proxy as D (data ..) -- import only the data namespace
This avoids needing to name each item individually, but otherwise has the same effect as writing out an explicit import list, like this:
import qualified Data.Proxy as T (type Proxy) -- import only the Proxy type
import qualified Data.Proxy as D (data Proxy) -- import only the Proxy constructor
The data
namespace specifier replaces the
existing limited use of pattern
in import lists, guarded by PatternSynonyms
.
Moreover, for consistency this proposal modifies proposal #65
to use data
rather than value
as the keyword for the data namespace in
fixity declarations and pragmas.
This proposal does not directly make changes to the tick syntax, or provide an equivalent at use sites. However it should reduce the need for disambiguating promoted data constructors using ticks, because qualified imports can be used instead.
1.2 Proposed Change Specification¶
1.2.1 Allow namespace specifiers with wildcards in import/export lists¶
When ExplicitNamespaces
is enabled, anywhere the type
keyword may appear
in an import or export list, the data
keyword may also appear. Call such an
occurrence a namespace specifier. Any import/export of an identifier with a
namespace specifier will be taken to refer only identifiers in the given
namespace. It is an error to use a namespace specifier if the identifier is not
in scope in the given namespace.
Moreover, a namespace specifier may be followed by a ..
wildcard instead of
a single name. This is equivalent to importing or exporting all the available
names in the corresponding namespace.
More precisely, the existing grammar of import/export items accepted by GHC is essentially the following (after some minor simplifications):
export -> qcname_ext ['(' qcname_ext_w_wildcard_1, ..., qcname_ext_w_wildcard_n ')']
| 'module' modid
| 'pattern' qcon -- with PatternSynonyms
qcname_ext_w_wildcard -> qcname_ext
| '..'
qcname_ext -> qvar
| qtycon
| 'type' oqtycon -- with ExplicitNamespaces
This proposal redefines qcname_ext
as follows:
qcname_ext -> qvar
| qtycon
| 'type' oqtycon_w_wildcard -- with ExplicitNamespaces
| 'data' qvarcon_w_wildcard -- with ExplicitNamespaces
oqtycon_w_wildcard -> oqtycon | '..'
qvarcon_w_wildcard -> qvarcon | '..'
Notice that:
module
andpattern
are valid only at the top level of the export, whereastype
anddata
are valid either at the top or nested inside a type constructor or typeclass name.data
may be followed by a data constructor name, a variable name (including record selectors, in particular), or a`..
wildcard.Where a parent type constructor or class is exported together with its children, any namespace specifier on an individual import/export item will apply only to the parent; the children are unrestricted. For example,
import M (type T(..))
imports bothT
in the type namespace and any children in either namespace.
1.2.2 Dodgy import/export warnings¶
The existing -Wdodgy-imports
and -Wdodgy-exports
flags (part of the
-W
warning group) emit warnings if a ..
wildcard does not refer to any
names in scope. For example this arises in an export of T(..)
if T
is a
type synonym or a data type with no constructors.
Similarly, under this proposal a warning will be emitted if a type ..
or
data ..
item does not refer to any names in the corresponding namespace.
For example:
import M (data D(..))
,import M (data D(type ..))
andimport M (data D(data ..))
are syntactically valid, but will always give rise to a warning, as it is not currently possible for identifiers in the data namespace to have children. (We might imagine changing this, e.g. for record fields, but doing so is outside the scope of this proposal.)import M (type T (data ..))
is accepted. WhenT
is a type its only sub-items are in the data namespace so this is somewhat redundant, but a class may have both children in either namespace.Similarly,
import M (type T (type ..))
is accepted but will give rise of a warning whenT
is a type or a class that does not have any associated types.
1.2.3 Deprecate use of pattern
in import/export lists¶
Since the data
specifier introduced above subsumes uses of the pattern
keyword in import/export lists that are permitted under PatternSynonyms
, we
propose a new warning -Wpattern-namespace-specifier
that warns when the
pattern
namespace specifier is used.
Initially this warning will be added to -Wcompat
. Three releases after this
proposal is implemented, the warning will be added to -Wall
.
We do not currently propose increasing the severity of the warning beyond
-Wall
, or removing support for pattern
in import/export lists entirely,
because the simplification to the compiler does not seem worth the backwards
compatibility cost.
1.2.4 Use data
specifier in fixity declarations and WARNING
/DEPRECATED
pragmas¶
This proposal changes GHC Proposal #65
to use the data
namespace specifier instead of value
. (The specific
changes are thanks to Vladislav Zavialov and form part of the PR.)
That proposal has not yet been implemented, so this is not a breaking change.
1.3 Examples¶
1.3.1 Export lists with namespace specifiers¶
{-# LANGUAGE ExplicitNamespaces, TypeFamilies #-}
{-# OPTIONS_GHC -Wpattern-namespace-specifier #-}
module M
( D -- Accepted: exports data family D
, data D -- Accepted: exports data constructor D
, C(type D) -- Accepted: exports class C and data family D
, C(type ..) -- Accepted: exports class C and data family D
, C(data ..) -- Accepted: exports class C and method m
, D(data f) -- Accepted: exports data family D and field f
, D(type ..) -- Accepted: exports data family D; -Wdodgy-exports warning
, pattern D -- Accepted: exports data constructor D; -Wpattern-namespace-specifier warning
, T(data D) -- Accepted: exports type T and data constructor D
, data f -- Accepted: exports field f
, data v -- Accepted: exports term v
, type T (..) -- Accepted: exports type T and all its data constructors D, D2
, type T (data ..) -- Accepted: exports type T and all its data constructors D, D2
, type T (type ..) -- Accepted: exports type T; -Wdodgy-exports warning
, T(pattern D) -- Rejected: pattern keyword cannot be used in sub-list
, data T -- Rejected: T not in scope in data namespace
, type E -- Rejected: E not in scope in type namespace
, type .. -- Accepted: exports data family D, C, T
, data .. -- Accepted: exports data constructor D, m, E, f, D2, v
) where
class C a where
data D a
m :: a
instance C Int where
data D Int = E { f :: Int }
data T = D | D2
v = ()
{-# LANGUAGE ExplicitNamespaces #-}
module M
( (+) -- Accepted: exports value-level function
, data (+) -- Accepted: exports value-level function
, type (+) -- Accepted: exports type family
) where
import Prelude (data (+))
import GHC.TypeLits (type (+))
{-# LANGUAGE ExplicitNamespaces #-}
module M
( type (+++) (data X) -- Accepted: exports data type (+++) and its constructor
, (+++) (X) -- Rejected: variable (+++) cannot have a sub-list
) where
(+++) = (+)
data a +++ b = X
module M
( type .. -- Accepted: exports T
, data .. -- Accepted: exports MkT
) where
data T = MkT
1.3.2 Imports with namespace specifiers¶
In the following examples, recall that the Data.Proxy
module defines data
Proxy t = Proxy
. (Its other exports are ignored for the purposes of these
examples.)
The same module can be imported with different qualifiers for the type namespace and data namespace:
{-# LANGUAGE ExplicitNamespaces #-}
import Data.Proxy as T (type ..)
import Data.Proxy as D (data ..)
-- This is accepted:
f :: T.Proxy Int
f = D.Proxy
-- This is accepted too, because both names are in scope unqualified:
g :: Proxy Int
g = Proxy
-- This is rejected, because the type T.Proxy cannot be used at the term level:
h :: T.Proxy Int
h = T.Proxy
{-# LANGUAGE ExplicitNamespaces, ImportQualifiedPost #-}
import Data.Proxy qualified as T (type ..)
import Data.Proxy qualified as D (data ..)
-- This is accepted:
f :: T.Proxy Int
f = D.Proxy
-- This is rejected, because the names are in scope only with qualifiers:
g :: Proxy Int
g = Proxy
{-# LANGUAGE ExplicitNamespaces #-}
import Data.Proxy (type Proxy(..)) -- Accepted: imports both type constructor and data constructor
import Data.Proxy (type Proxy(data ..)) -- Accepted: imports both type constructor and data constructor
import Data.Proxy (type Proxy(type ..)) -- Accepted: imports type constructor; -Wdodgy-imports warning
{-# LANGUAGE ExplicitNamespaces #-}
module M
( type T (..) -- Accepted: exports T and MkT
) where
data T = MkT
{-# LANGUAGE ExplicitNamespaces #-}
module N where
import M (T (data MkT)) -- Accepted: imports both T and MkT
import M (T (data ..)) -- Accepted: imports both T and MkT
import M (T (type MkT)) -- Rejected: MkT is not in the type namespace
import M (T (type ..)) -- Accepted: imports T only; -Wdodgy-imports warning
import M (type T (..)) -- Accepted: imports both T and MkT
import M (data T (..)) -- Rejected: T is not in the data namespace
1.4 Effect and Interactions¶
This proposal makes ExplicitNamespaces
more coherent and more useful for
avoiding punning via qualified imports.
In either an import or an export list, it is possible to import or export the
entire contents of a module’s type namespace or data namespace.
Importing/exporting type .., data ..
is equivalent to omitting the
import/export list, except that it will not emit a -Wmissing-import-lists
or
-Wmissing-export-lists
warning.
Since there are two disjoint namespaces, import M hiding (type ..)
is
equivalent to import M (data ..)
. We permit both, however.
1.4.1 TypeData
¶
When TypeData (proposal #106) is in use, it introduces both the type constructor name and any data constructor names into the type namespace only (and does not permit punning). For example:
{-# LANGUAGE TypeData #-}
{-# LANGUAGE ExplicitNamespaces #-}
module M
( type T (type MkT) -- Accepted: exports both T and MkT
, data MkT -- Rejected: MkT not in data namespace
, type .. -- Accepted: exports both T and MkT
) where
type data T = MkT
1.4.2 PatternSynonyms
¶
Referring to pattern synonyms in top-level import/export items requires the
data
namespace specifier (or the deprecated pattern
keyword).
Alternatively, pattern synonyms and their record fields can be associated with
parent type constructors by being mentioned in export sub-lists.
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -W #-}
module M
( type T (data P, data f) -- Accepted: associates P and f with T
, data P -- Accepted
, data P (f) -- Rejected: f is not a child of P
, P -- Rejected: P not in scope in type namespace
, pattern P -- Accepted; -Wpattern-namespace-specifier warning
) where
data T = MkT Int
pattern P {f} = MkT f
{-# LANGUAGE ExplicitNamespaces #-}
module N where
import M (P) -- Rejected: P not in scope in type namespace
import M (data P) -- Accepted
import M (T(..)) -- Accepted: imports T, P and f
import M (type T(type ..)) -- Accepted: imports T only; -Wdodgy-imports warning
1.5 Costs and Drawbacks¶
This proposal introduces new syntax for namespace specifiers with wildcards,
however its meaning is consistent with the existing namespace specifiers on
individual import items. By making the ExplicitNamespaces
extension more
consistent it should become easier to learn.
The implementation and maintenance cost of this proposal is expected to be relatively low.
1.6 Backwards Compatibility¶
This proposal is mostly backwards compatible, except that code may
theoretically rely on using type
with a data constructor, which is
accepted in existing GHC versions but rejected under this proposal (see GHC
issue #22581). For
example, the following import is accepted today but will be rejected under
this proposal:
import Data.Functor.Product ( Product ( type Pair ) )
Given that this is essentially a bug, it seems unlikely that user code relies
on it. Moreover it can be fixed by simply removing the bogus type
namespace specifier. Thus we do not expect significant breakage from this
change.
Existing code that uses the pattern
keyword (with PatternSynonyms
) in
import/export lists and uses -Wcompat
(or eventually -Wall
) will
receive a warning from -Wpattern-namespace-specifier
until it migrates to
use data
instead (with ExplicitNamespaces
). In the future we may wish
to remove support for pattern
as a namespace specifier entirely, but doing
so is not part of this proposal, so this is not a breaking change.
1.7 Alternatives¶
The original version of this proposal extended the import declaration itself
with a namespace specifier (e.g. import M type as T
). The current revision
instead makes this part of the import list (e.g. import M as T (type ..)
).
The revised version has several advantages over the original:
The meaning of
import M (type ..)
is arguably more obvious thanimport M type
.type ..
ordata ..
can now be used in an export list, whereas the original proposal did not have a way to export all names in a single namespace.The original version had a subtle distinction between
import M type (T(..))
andimport M (type T (..))
.The original version had redundant constructions such as
import M type (type T)
and had to rule out inconsistencies such asimport M data (type T)
.
There are various other alternative possibilities:
We could imagine supporting
import M (..)
for consistency, however this would be entirely redundant as it is equivalent toimport M
. Ideally the implementation would issue a sensible error in this case.We could use
value
,term
,pattern
, or any other keyword instead ofdata
to denote the data namespace. It seems preferable to usedata
as it is an existing keyword (unlikevalue
andterm
) and unlikepattern
it more clearly refers to the data namespace. However this may lead to beginner confusion if expressions likeimport M (data f)
are used, sincedata
refers to the namespace rather than a datatype.There are alternatives to the import syntax proposed here, for example proposal #340 proposes
import M as (T, D)
syntax. More examples of alternative syntax:import M as {T, D}
import M type as T data as D
import M as (type T, data D)
Instead of introducing new syntax we could use the existing syntax for explicit import lists:
import Data.Proxy( Proxy ) qualified as T import Data.Proxy( pattern Proxy ) qualified as D
However it is significantly less convenient: you can’t import all the things at once without manually listing every single one of them.
This proposal does not introduce a way to disambiguate namespaces at use sites, corresponding to the prefix
'
syntax used byDataKinds
and the prefix'
and''
used byTemplateHaskell
. Instead, disambiguation must happen at the level of imports, and cannot be used for definitions within the same module. Proposal #214 suggested using prefixdata.
andtype.
systematically for this purpose. However, that would be syntactically noisy and there was difficulty gathering consensus for that approach.Rather than modifying
ExplicitNamespaces
, we could introduce a new extension. This would make it clearer whether code depends on the new feature, or whether the older version ofExplicitNamespaces
was enough. However the general consensus seems to favour reducing the number of extensions over avoiding change to existing extensions.GHC currently rejects an attempt to import/export a data constructor or pattern synonym at the top level, such as in the following, with a “Not in scope: type constructor or class” or “Module … does not export” message:
module M ( D ) where data T = D
In principle there is no ambiguity here, so this could be accepted without requiring the
data
keyword. Similarly, exporting a type operator currently requires thetype
specifier, even if there is no conflicting term-level operator. However these cases are comparatively rare, and the keywords make the program clearer to the reader, so we propose continuing to require them. GHC’s error messages should be improved to point users in the right direction (see GHC issues #20007, #21826).import M (T(data D))
is technically redundant asimport M (T(D))
refers to the constructorD
by default. It is currently necessary to usedata
as a namespace specifier only at the top level. However it seems best to allowdata
to be used consistently withtype
.
1.8 Unresolved Questions¶
None
1.9 Implementation Plan¶
Support with the implementation of this proposal would be welcome.