Would appreciate any feedback on this new proposal. Thank you in advance.
Forgive me if this is explained in the proposal as I have only took a very brief look, but would this be subsumed by the local modules proposal?
If a module A defines an instance, and module B does an scoped import of A for some function, would the instance defined in A be available in the entirety of B?
Yes it is subsumed by LocalModules.
Updated the proposal to explicitly state that ScopedImports does not change how instance/module resolution works already and doesnāt affect parts of the grammar like class declarations, instance declarations, pattern synoynms, or any other top-level declarations. Syntactically, ScopedImports only affects where import declarations are allowed. These special import declarations are called scoped import declarations.
Also, updated the proposal based on a commenters idea to force users to explicitly import at least the instances of the module they are using in a scoped import declaration. This simplifies the proposal implementation and aligns with the instance semantics of Haskell.
Hopefully, this clears up some confusion. Still a lot of different interactions to think about and add to the proposal.
Also added a section on how this is different from the LocalModules proposal.
Updated the proposal and renamed the extension from ScopedImports to LocalImports
If we are going to allow import locally in a let/where I think we should also allow import anywhere in a module. The only(?) current argument for keeping them just at the top is that then you can easily find them. But if we start allowing local import then they will be all over the place, so we might as well allow it on the top level as well. I think that will also simplify the grammar.
The current proposal still requires a top level import of any module that is also imported locally. The proposed local imports are mainly a way to change the visibility of imported names.
One of the reasons for still requiring top-level imports at the top of the module is so the Downsweep.hs part of the GHC pipeline remains unchanged. Downsweep.hs only has to parse the top of the module to build a module graph. If we allow users to place top-level imports anywhere within the module file, Downsweep.hs will need to be modified to parse the entire file for import declarations.
The fact that current GHC needs some small modification to handle imports anywhere is a very weak reason. The entire module has to be parsed sooner or later, might as well do it sooner. We should decide on whatās good for the language, not the implementation. Thereās a good argument for keeping imports at the top for readability, so Iām not going to argue for imports scattered everywhere. (MicroHs allows imports anywhere, because it was simpler to allow it.)
Good point. The original proposal didnāt require top-level imports at all. The downsweep algorithm can be modified so that it looks at every expression in the module for import declarations (even more complex than just looking at top-level decls). Some concerns from the community were:
-
Additional complexity
-
Concerns about tooling (like stackās script command)
-
Instance scope isnāt explicit. For example:
{-# LANGUAGE LocalImports #-} module M where -- Are instances locally scoped here too? f = let import Data.List (sum) where sum [1, 2, 3]Requiring a top-level import, at least an empty one, makes it explicit that instances are in the
global scope:{-# LANGUAGE LocalImports #-} module M where import Data.List () -- instances are global f = let import Data.List (sum) in sum [1, 2, 3]I personally think it would be better if users of this extension just assume that if a module is imported locally, then all of itās instances are in the global scope. That seems simple enough to me.
{-# LANGUAGE LocalImports #-} module M where -- If you use a module in a local import -- its instances are in the global scope f = let import Data.List (sum) in sum [1, 2, 3]
And I thought increased compile times were a negative too. Probably not worth thinking about. Any implementation we go with will probably increases compile times for modules that use the extension by a negligible amount.
I agree.
This brings me back to the original iteration of the proposal. Letās not require top-level imports at all, unless you want to bring a name or module qualifier into the global scope. Tooling and such will probably be affected either way. As long as the language is improved.
I donāt know about this. I guess itās nice to know that whatever module you open, the imports will be at the top. This is consistent. On the other hand, itās kind of annoying to have to go to the top of a module in case you need to remember what imports are in scope or what qualifiers represent what modules. If you have good working memory, this probably doesnāt affect you.
When writing code at the bottom of the file you may think āI need this import!ā, then you go back to the top of the module to add the import, and then go back down to where you were writing code. Then you may think āI actually donāt need this import!ā, then you have to go back to the top and back down. This could be a skill issue. People who know their IDE well arenāt affected by this.
I just feel I should say that Hazyās import system is one of the things Iām quite proud of. Not only do I fully support local imports without the top level import requirement, but I also have proper recursive modules without hs-boot files and I even have lazy importing. If you have an import without wildcards where you donāt use the imports, the module never even gets opened.
Here is some code actual code from my tests:
module Sloppy where
import Nonexistent (unused)
value, value2 :: ()
value = ()
value2 = value
A big reason why Iām able to do this is by not supporting orphan instances whatsoever. If orphan instances donāt exist, then by definition if a class and type are in scope, then all itās instances are too. The problem of scoped instances just goes away. Orphan instances really shouldnāt be allowed.
This reminds me I still have to think about the interaction between local imports and orphan instances for this extension. Hopefully itās not a blocker.
Thatās cool that Hazy Haskell doesnāt have them!
So if someone writes a serialization library like aeson and you want to use it for some data type in an existing library, then you are just stuck, right?
Well you could use newtypes wrappers. Your not stuck, just massively annoyed.
I have a few small arguments against keeping imports strictly at the top.
-
It makes generating Haskell code harder, because you have to aggregate imports separately from the things that use them.
-
Making dependency analysis slightly easier isnāt a compelling reason to hold back improvements in the usability of the module system. If thatās really affecting performance, Iād rather have a separate way to spell out dependencies.
-
Itās frankly just silly that āliterateā Haskell blog posts always start with something like āPlease ignore these imports.ā Imports are like entries in an index or bibliography, which usually go either at the end of the text, or near the text that cites them.
I think local imports in let/where are a great idea. For a section of a file with a group of bindings that share common imports, itād also be nice to allow top-level imports closer to their use sites, because it is a pain for me to split a small project into several files while Iām prototyping.
Although if you also wanted to keep top-level imports from clogging up the scope of the whole module, itād go better with top-level let {ā¦} in {ā¦}.
module Language
(
-- * AST
Program,
Statement,
Expression,
-- * Parsing
lex,
parse,
-- * Evaluation
run,
eval,
) where
-- | AST
let ⦠in
data Program where ā¦
data Statement where ā¦
data Expression where ā¦
-- | Parsing
let import Text.Megaparsec in
lex = ā¦
parse = ā¦
program = ā¦
statement = ā¦
expression = ā¦
-- | Evaluation
let import Data.IORef in
run = ā¦
eval = ā¦
I am strongly in favour of this proposal
.
While I like the idea of having all the imports at the top - because it helps with some refactorings - for modules where all imports are spelled explicitly, the top of the file becomes unwieldy very fast. I often need to scroll the first screen to start seeing the actual code. I would welcome having imports closer to where they are used.
I would use this feature mostly to be able to do scoped unqualified imports for functions that need to use a specific library. An example would be something like:
processText = ...
where import Data.Text
which is much nicer than putting import Data.Text qualified as T at the top in a module that may have little to do with processing text.
But I think having to put imports both at the top and locally where the imported identifiers are used would add a lot of redundancy and friction to the feature. You lose brevity, which is (in my opinion) the most appealing things about this proposal.
I like imports at the top.
I find it can give an immediate impression of which APIs code uses and where in the program structure it sits.
Python is the only language I know and somewhat regularly use that allows imports āanywhereā. While I have used local imports in python in throwaway scripts and the like I always found putting them anywhere but the top of the file is more likely than not to make a mess of things in the long run. So I think itās generally a bad idea to use them only locally. To the point where I really couldnāt say if making top level imports optional would actually be better for the language.
I wonder what other people with experience in languages that actually allow local imports think of this.
On the other side people write throw-away scripts or small programms in Haskell too and for that local imports would be nice to have. After all it should be easy to enforce required top level imports via HLint or similar for most projects where they are deemed inappropriate.
So while I donāt think very highly of local imports without a corresponding top level import, maybe they do have a place in the language.