1 Import shadowing¶
Local binders are allowed to shadow names defined in outer scopes. Here, we propose that top-level binders should also be allowed to shadow imported names, basically treating imported modules as living in a scope that is “outside” the top-level one.
1.1 Motivation¶
Currently, there is no way in Haskell to shadow an imported name in
the top-level scope. The only workaround is to avoid importing said
name at all. For example, if we want to use some Control.Exception
functions while also defining our own catch
, we have to import the
former with an explicit hiding of catch
:
module Example1 where
import Control.Exception hiding (catch)
catch :: Fish -> IO Food
catch fish = ...
goFishing :: IO Food
goFishing = bracket goToCabin goHome $ catch salmon
Without hiding catch
, this results in a name resolution conflict
in the definition of goFishing
:
Ambiguous occurrence `catch'
It could refer to
either `Control.Exception.catch',
imported from `Control.Exception' at Example1.hs:1:1-24
(and originally defined in `GHC.IO')
or `Example1.catch', defined at Example1.hs:7:1
Our proposal is an alternative name resolution policy exposed as a
language extension tentatively named ImportShadowing
. It allows
definitions from the current module’s top-level scope to shadow names
from imported modules. So in this example, the occurrence of catch
in the definition of goFishing
resolves to the definition in
Example1
even when catch
is not explicitly hidden from the
import list of Control.Exception
.
We believe this is an improvement to the status quo because it is a natural extension of the idea behind the shadowing policy of local binders: that shadowing expresses the intention by the user to only care about the names that are defined “more nearby”. A cursory search on Stack Overflow finds lots of Haskell users who implicitly expected imports to be shadowed by top-level definitions:
A second-order effect of the proposed extension is that it can lead to
preemptive forward compatibility. Adding a new export to Prelude
can lead to breakage just by virtue of existing code defining and
using top-level definitions with the same name. With
ImportShadowing
, the existing intra-module references keep their
meaning and there is no migration needed to accomodate the new
Prelude
names.
1.2 Proposed Change Specification¶
A new language extension ImportShadowing
is added.
When ImportShadowing
is enabled, the following changes take place:
1.2.1 Resolution of references in module body¶
Consider an occurrence of an unqualified name x
, not bound locally
(by let
, lambda, a case
alternative, etc). There are two
possible sources of resolving it:
If there is a top-level binding of
x
then the occurrence is resolved to that binding.If the import declarations bring into scope a unique entity with unqualified name
x
, the occurrence is resolved to that entity.
Consider an occurrence of a qualified name M.x
:
If the module is called
M
and there is a top-level binding ofx
, the occurrence is resolved to that bindingIf the import declarations bring into scope a unique entity with qualified name
M.x
, the occurrence is resolved to that entity.
In both cases, Haskell 2010 regards cases (A) and (B) on equal footing as per Section 5.5.2: if exactly one of the two cases can be used to resolve the name, that case is used; if both cases can be used, then the occurrence is ambiguous and reported as such.
Instead, we propose that when ImportShadowing
is enabled,
(A) and (B) are tried in order, i.e. if the (A) case resolves the
occurrence, then that is used, and the (B) case is only checked
otherwise.
1.2.1.1 Alternative perspective: desugaring into let
bindings¶
In Haskell 2010, all imported names and all top-level definitions in the current module together make up a single unified top-level scope. With this proposed alternative policy, there are two top-level scopes instead: one consisting of all imported names, and a second one, under this first one, that consists of all top-level definitions from the current module.
To model these two name resolution approaches, we can desugar the
Haskell 2010 name resolution policy for a given module to a single
nested let
-block, e.g. for the following program:
module Mod (fun1, fun2) where
import M1 hiding (overridden)
import qualified M2
overridden = ... importedFromM1 ...
fun1 = ... overridden ...
fun2 = ... M2.importedFromM2 ... fun1 ...
we can write out its explicit scoping as:
let
-- imports from M1
importedFromM1 = ...
-- imports from M2
B.importedFromM2 = ...
-- defined in Mod
overridden = ... importedFromM1 ...
fun1 = ... overridden ...
fun2 = ... M2.importedFromM2 ... fun1 ...
in
-- exports of Mod
(fun1, fun2)
With our proposed scheme, the same program with ImportShadowing
turned on can be modeled as a two nested let
blocks:
let
-- imported from M1
importedFromM1 = ...
-- imports from M2
B.importedFromM2 = ...
in
-- defined in Mod
let
overridden = ... importedFromM1 ...
fun1 = ... overridden ...
fun2 = ... M2.importedFromM2 ... fun1 ...
in
-- exports of Mod
(fun1, fun2)
Of course, in this example, there is no observable difference between
the two desugarings, since our module Mod
was already well-scoped
with the Haskell 2010 shadowing rules. However, if we change the
program slightly by importing all of M1
wholesale:
module Mod (fun1, fun2) where
import M1
import qualified M2
overridden = ... importedFromM1 ...
fun1 = ... overridden ...
fun2 = ... M2.importedFromM2 ... fun1 ...
then the desugaring using Haskell 2010 semantics leads to the
following invalid program (note the two bindings of overridden
in
the same let
):
let
-- imports from M1
importedFromM1 = ...
overriden = ...
-- imports from M2
M2.importedFromM2 = ...
-- defined in Mod
overridden = ... importedFromA ...
fun1 = ... overridden ...
fun2 = ... M2.importedFromM2 ... fun1 ...
in
-- exports of Mod
(fun1, fun2)
Whereas the ImportShadowing
version is valid:
let
-- imported from M1
importedFromM1 = ...
overridden = ...
-- imports from M2
M2.importedFromM2 = ...
in
-- defined in Mod
let
overridden = ... importedFromM1 ... -- This shadows the imported "overridden"!
fun1 = ... overridden ...
fun2 = ... M2.importedFromM2 ... fun1 ...
in
-- exports of Mod
(fun1, fun2)
1.2.2 Export lists¶
References in a module’s export specification are resolved in the same scope as that used for references in the module body, as per Resolution of references in module body. For example if we have something like
module A (foo) where
import M -- This exports "foo"
foo = ...
then the foo
exported by A
should be the one defined in
A
’s top-level.
When modules are reexported wholesale, shadowing doesn’t come into
play, and so we keep the behaviour without this extension: the form
module M
names the set of all entities that are in scope with both
an unqualified name e
and a qualified name M.e
. Example:
module A (module M) where
import M -- this exports "foo"
foo = ...
Here, it is M.foo
that is (re-)exported by A
, not A.foo
.
If both module M
and foo
are exported, then that is a
conflicting export error, and should be reported the same way as
conflicts between exporting module M1
and module M2
without
this extension. Example:
module A (foo, module M) where
import M -- this exports "foo"
foo = ...
This should report a conflict between the export items foo
(resolving to A.foo
) and M.foo
.
1.2.3 Warnings¶
Top-level bindings that shadow imported names should be regarding as
shadowing bindings for the purposes of -Wname-shadowing
.
1.3 Examples¶
This extension shines especially when shadowing names defined in the
Prelude
, since hiding Prelude
imports otherwise requires
changing to an explicit import for Prelude
: we can go from
module Mod where
import Prelude hiding (zip)
zip = ...
to just
module Mod where
zip = ...
The above example is taken directly from the “Import” page of the Haskell Wiki.
1.4 Costs and Drawbacks¶
The usual drawback of language extensions leading to some language fragmentation.
Users new to Haskell seem to find this idea intuitive. We have gathered decade+-long experience with a Haskell compiler that uses import shadowing (and doesn’t even let users turn it off), with a Haskell code base of several million lines of code that sees work from both experienced Haskell developers as well as people with a non-software-engineering background whose introduction to Haskell was via this compiler. There’s no record of either novices (learning only the import-shadowing behaviour) or experienced Haskellers (who are used to imports being in the same scope as top-level definitions) ever getting into trouble due to this difference to Haskell 2010.
1.5 Backward Compatibility¶
Haskell 2010 doesn’t have a mechanism for shadowing imported names,
and valid Haskell 2010 programs retain their exact meanings with
ImportShadowing
turned on. The proposed extension only makes
previously unaccepted programs accepted by the scope checker.
So this is a “-1”-impact change: it doesn’t break existing code, and “un-breaks” existing broken code.
1.6 Alternatives¶
1.6.1 Status quo¶
Before this proposal, there are two alternative ways of referring to names defined at the current module’s top level:
The imported names we want to shadow can be hidden from the import itself, using the
import SomeModule hiding (someName)
syntaxThe current module’s name can be used to qualify names, i.e.
CurrentModule.someName
instead of justsomeName
.
1.6.2 Ordered imports¶
Other languages like OCaml or Agda have a linear top-level scope. The
Haskell equivalent of this would be that later import
statements
and top-level bindings shadow earlier ones. By way of example,
supposing foo
is exported by all of A
, B
, and C
:
module Mod where
import A
import B
-- Here, "foo" resolves to "B.foo"
foo = ...
-- Here, "foo" resolves to "Mod.foo"
import C
-- Here, "foo" resolves to "C.foo"
This would be a complete departure from Haskell’s usual permutation
invariance of definitions. It is this proposal author’s opinion that
this would be too large a change to be up to the addition of a mere
LANGUAGE
pragma.
A full proposal for this would also need to answer hairy questions like:
If
Mod
exportsfoo
, whichfoo
does that resolve to?Can I import
A
again to make itsfoo
shadowC.foo
?Is it allowed to re-bind
foo
inMod
if there areimport
statements between it and the previous binding offoo
?
1.7 Unresolved Questions¶
_None came up in the proposal discussion_
1.8 Implementation Plan¶
For GHC specifically, it already has a similar name resolution policy,
only used by the GHCi REPL. Implementing ImportShadowing
is as
easy as switching to the GHCi shadowing mechanism, plus some extra
fiddling around disambiguating exported names.
For other Haskell compilers, the implementation plan depends on their current name resolution infrastructure.
1.9 Endorsements¶
As mentioned in the Drawbacks section, we have positive
experience in a setting where ImportShadowing
is always on in a
large Haskell code base with lots of developers over a long time.