Draft GHC Proposal: ScopedImports

Would appreciate any feedback on this new proposal. Thank you in advance.

Discussion

6 Likes

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?

4 Likes

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.

My thoughts on it so far

Yes, this is correct.

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

1 Like

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.

3 Likes

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.

1 Like

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.)

5 Likes

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.

2 Likes

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!

1 Like

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.

1 Like

I have a few small arguments against keeping imports strictly at the top.

  1. It makes generating Haskell code harder, because you have to aggregate imports separately from the things that use them.

  2. 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.

  3. 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 = …
3 Likes

I am strongly in favour of this proposal :+1: .

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.

2 Likes

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.

3 Likes