purescript icon indicating copy to clipboard operation
purescript copied to clipboard

Allow modules to re-export modules as qualified

Open garyb opened this issue 9 years ago • 14 comments

Suggestion from @natefaubion.

Something vaguely like:

module MyPrelude
  ( module Data.Map as M
  ...
  ) where

import Data.Map

Then you can:

import MyPrelude

myInsert = M.insert

or:

import MyPrelude as MP

myInsert = MP.M.insert

etc.

After giving it some thought, I think it would be pretty nice to have. I'm not sure exactly how we'd want to deal with the syntax for making it work in the exports though - I'd prefer not to have as M in the exports (using the same naming within the module as with what it exports is better I think), but also I don't think we want to automatically include qualification when an import has it, as it will prevent the current useful behaviour of:

module A
  ( ...
  , module Exports
  ) where

import Foo (x, y, z) as Exports
import Bar (a, b, c) as Exports

garyb avatar Dec 12 '16 15:12 garyb

FWIW, this is something that's been requested several times in IRC by various people. It would be a really nice thing to have for custom Preludes to prevent name clashes. The desugaring is fairly "simple" in that exporting a qualified alias would just be elaborated to a qualified import in the downstream module.

natefaubion avatar Dec 12 '16 15:12 natefaubion

ie

module A (export Data.Map as Map) where

module B where
  import A
module A (export Data.Map as Map) where

module B where
  import A
  import Data.Map as Map

This does have issues with the import (a, b, c) as Exports case though.

natefaubion avatar Dec 12 '16 15:12 natefaubion

I would really like to have qualified module re-export


I would also like being able to know easilly where does a module comes from.

import MyPrelude --  <- should there be a warning  ?
myInsert = M.insert
import MyPrelude(module M) --  <- and an re-exported module syntax ? 
myInsert = M.insert

side note:

if we plan to have a prelude with lots of qualified re-exports such as

import MyPrelude
x = BigInt.fromInt 9999999
y = Color.fromInt 0xffffff

One could even adopt C style naming convention:

module MyPrelude where
import BigInt as BigInt
import Color as BigInt
bigInt_fromInt = BigInt.fromInt
color_fromInt = Color.fromInt

and voilà:

import MyPrelude
x = bigInt_fromInt 9999999
y = color_fromInt 0xffffff

I did an experiment a while back because I was really annoyed not to have module reexport in haskell:

from a definition file here: https://github.com/rvion/ride/tree/master/jetpack#modules

m Data.Map.Strict
lm Data.Map
set Data.Set
hm Data.HashMap.Strict
ne Data.List.NonEmpty
t Data.Text
t Data.Text.Encoding
t Data.Text.IO
lt Data.Text.Lazy
...

I was generating thouthands of non-clashing names

image

module Codec.Archive.Tar.Entry.AsTar
  ( module Codec.Archive.Tar.Entry.AsTar
  ) where
-- generated by https://github.com/rvion/ride/tree/master/jetpack-gen

import qualified Codec.Archive.Tar.Entry as I

-- tar_getDirectoryContentsRecursive :: FilePath -> IO [FilePath]
tar_getDirectoryContentsRecursive = I.getDirectoryContentsRecursive

-- tar_packDirectoryEntry :: FilePath -> TarPath -> IO Entry
tar_packDirectoryEntry = I.packDirectoryEntry

-- constructor :: GnuFormat
tar_mk'GnuFormat =  I.GnuFormat
pattern TarGnuFormat  <-  I.GnuFormat

type TarLinkTarget  = I.LinkTarget

type TarOwnership  = I.Ownership
get_tar_ownerName o = I.ownerName o
set_tar_ownerName x o = o { I.ownerName = x}
get_tar_groupName o = I.groupName o
set_tar_groupName x o = o { I.groupName = x}
get_tar_ownerId o = I.ownerId o
set_tar_ownerId x o = o { I.ownerId = x}
get_tar_groupId o = I.groupId o
set_tar_groupId x o = o { I.groupId = x}

-- constructor :: String -> String -> Int -> Int -> Ownership
tar_mk'Ownership =  I.Ownership
pattern TarOwnership a b c d <-  I.Ownership a b c d

type TarPermissions  = I.Permissions
-- ... 

the tool had reexport collision detection, module priority, a cli. GHC8 pattern synonyms improvements along some other fix + custom renaming rules would have make it quite a productivity tool / prelude replacement builder.

I tested it on some side (private) projects and I was really more productive with this mega prelude ! -> That's why I like the idea, no matters how it is done

rvion avatar Dec 12 '16 16:12 rvion

Re:

import MyPrelude --  <- should there be a warning  ?

Nah, it'd follow the same rules as normal - one open import is allowed.


if we plan to have a prelude with lots of qualified re-exports such as

I don't see us using this at all in the core Prelude, and probably not any of the core libraries either.

That underscore-qualified naming scheme looks horrendous to me personally :smile: I'd just use the actual qualification I imagine.

garyb avatar Dec 12 '16 16:12 garyb

That underscore-qualified naming scheme looks horrendous to me personally 😄 I'd just use the actual qualification I imagine.

that was one of the many problems 😄 I tried many things to make it nicer, replacing the _ with ' to mimic the ., etc, but in the end, it was still ugly.

(this being said, typing Tar.extract is not really different to typing tar_extract or tar'extract)

There were more serious issues, though. Haskell doesn't preserve type aliases everywhere, documentation in the re-exported module was lacking, etc.

Anyway, the point was mostly about productivity boost of having somethings like re-exported modules:

✅ no name clash ✅ great autocompletion experience ✅ no need to stop coding to import a module or add a dependency ✅ clearer code for external people (function names were always explicit) etc.

rvion avatar Dec 12 '16 16:12 rvion

It seems like this would require lots of non-local decisions when checking for things like duplicate imports.

Is M.x a duplicate import? Well to decide that, I have to look through every open import to see if it exports a module qualified as M.

In an ideal world of course, there is only one open import, but that's not how everyone works. And if there is only one open import, then there is only one set of non-locally qualified modules to pull in in any given module, so the utility of this seems a bit limited.

Finally, this introduces new sorts of overlapping imports, where you have module X exporting module Y qualified. Then if there is any actual module called X.Y then another module can't import both X.Y and X as X.

I'm not completely against this, because I can see how it could be useful for certain types of work, but I'd really like to see a concrete proposal for how we'd extend the existing checks (and ideally leave it until after 1.0).

paf31 avatar Dec 12 '16 17:12 paf31

It seems like this would require lots of non-local decisions when checking for things like duplicate imports.

Is M.x a duplicate import? Well to decide that, I have to look through every open import to see if it exports a module qualified as M.

I didn't see this behaving as any different from how things are already resolved: we wouldn't check for clashing module namespaces, we'd just raise an error when an M.value reference is ambiguous. The same problems you're describing already apply with multiple open imports.

And if there is only one open import, then there is only one set of non-locally qualified modules to pull in in any given module, so the utility of this seems a bit limited.

Not at all! For SlamData at least there's a bunch of qualified modules we'd want to re-export from the prelude; maps, sets, bifunctor + profunctor operations (lmap/rmap conflicts without qualification as BF.lmap, etc).

Finally, this introduces new sorts of overlapping imports, where you have module X exporting module Y qualified. Then if there is any actual module called X.Y then another module can't import both X.Y and X as X.

Why not? We can already import X, Y, and Z as M. If it so happen that Z arrives via an open import it still works as long as the members don't conflict (see the description of the resolution of the first problem again).

I'm pretty sure this would be a minor extension to what we already have, with no additional cases needing to be handled, other than deciding how we want actually make it explicit that a re-export should be qualified.

garyb avatar Dec 12 '16 17:12 garyb

Something did occur to me about this scheme that might need additional handling... I'm not 100% sure though. I think we'd have to warn about potential hierarchical conflicts. For example, if you import a module as M, it means you can't safely import other things as M.X or M.Y etc, since M might be re-exporting modules under M._.

This means open imports become totally broken however, unless open imports don't bring in re-exported-named-modules.

But, I think the above may all be fine, following the rule of "in case of conflict take the explicit name, warn about the conflict" which we already use to resolve open vs explicit imports.

garyb avatar Jun 12 '17 15:06 garyb

I've been using purs-ide auto imports for the past few years, and I've consequently stopped caring about import boilerplate. Is this something still worth discussing? Should this be in "Ideas" instead?

natefaubion avatar Feb 06 '20 19:02 natefaubion

So I believe it would allow having merged type/namespace declarations:

module MyPrelude
  ( module Module.Cmd as Cmd
    module MoreExports
  ,
  ...
  ) where

import Module.Cmd (Cmd) as MoreExports

And importing MyPrelude would replace:

import Module.Cmd as Cmd
import Module.Cmd (Cmd)

This would be nice.

wclr avatar Dec 19 '20 19:12 wclr

@natefaubion

I've been using purs-ide auto imports for the past few years, and I've consequently stopped caring about import boilerplate.

I think this not so much about import boilerplate but about whole user ergonimics, and quite actual when creating EDSL where PS should supposedly shine.

wclr avatar Dec 20 '20 20:12 wclr

We use a lot of qualified imports, so this is still high on the wishlist for us.

drathier avatar Jul 12 '21 15:07 drathier