Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Namespace-qualified imports #340

Closed
wants to merge 7 commits into from

Conversation

int-index
Copy link
Contributor

@int-index int-index commented Jun 5, 2020

This proposal introduces a uniform way to specify the namespace (type or data) from whence a name comes. It does so by reusing an existing mechanism: module qualification.

The main idea is to allow compound aliases in imports:

import Data.Proxy as P       -- ordinary alias
import Data.Proxy as (D, T)  -- compound alias

In a compound alias, the first component qualifies the data namespace, and the second component qualifies the type namespace.

Rendered

@jvanbruegge
Copy link

A few comments:

  1. Why only make type constructors normal names and not their value counterparts? Using the {D, T} syntax should work for them too or am I wrong?
  2. A bit related to (1), it might be useful to import open only one of the two namespaces, AFAICT this is only possible by using a third module that reexports only one namespace
  3. I assume you can use _ to ignore the namespace you are not interested in, ie {_, T}, might be useful to state this
  4. Why couple the desugaring on certain imports? This looks quite ugly for me. Syntax should only change if you are enabling/disabling extensions, ie -XNoImplicitBuiltInTypes.
  5. How would disabling -XImplicitBuiltInTypes apply to error messages? The usefulness is somewhat limited when an error says something about [Int] (because of a resolved type alias) when I expect this to be List Int and confuse it with List Type

@int-index
Copy link
Contributor Author

int-index commented Jun 5, 2020

Thank you for the review @jvanbruegge

Why only make type constructors normal names and not their value counterparts? Using the {D, T} syntax should work for them too or am I wrong?

We could introduce Data.BuiltInData in addition to Data.BuiltInTypes for this purpose, but there seems to be no need. The idea behind Data.BuiltInTypes is that you could disable it with -XNoImplicitBuiltInTypes to remove the [] and (,) type constructors from scope. There doesn't seem to be a reason to remove data constructors (:), [], and (,) from scope. Even if there's such a reason, it does not arise from the motivation of this proposal.

Mentioned it in "Alternatives".

A bit related to (1), it might be useful to import open only one of the two namespaces, AFAICT this is only possible by using a third module that reexports only one namespace

Good point, I did not realize that. However, I can't come up with an example where that would be important, so I'm not too worried about it.

Mentioned it in "Unresolved Questions".

I assume you can use _ to ignore the namespace you are not interested in, ie {_, T}, might be useful to state this

I guess it won't hurt to add this. Added.

Why couple the desugaring on certain imports? This looks quite ugly for me. Syntax should only change if you are enabling/disabling extensions, ie -XNoImplicitBuiltInTypes.

I now realize I forgot to include the rules in Proposed Change Specification (fixed). The desugaring does not depend on imports per se – it depends on the namespace from which [] and (,) come.

For tuples, it's quite intuitive. (a, b) means (,) a b for whatever (,) is in scope (be it the type constructor or the data constructor). But for [a], the desugaring is different for the type constructor ([] a) and for the data constructor (a : []).

Both imports and -XNoImplicitBuiltInTypes can affect which (,) and [] are in scope.

How would disabling -XImplicitBuiltInTypes apply to error messages? <...> says something about [Int]

I included a requirement in the specification that demands helpful error messages in this case.

@jvanbruegge
Copy link

jvanbruegge commented Jun 5, 2020

We could introduce Data.BuiltInData in addition to Data.BuiltInTypes for this purpose

I thought more about having Data.BuilInTypes to contain definitions like data [] a = [] | a : ([] a) so you can get the types with import qualified Data.BuiltInTypes as {_, T}. Just thinking about removing this somewhat arbitrary choice to only export types.

I can't come up with an example where that would be important, so I'm not too worried about it.

It probably is if you take a look at explicit imports. At the moment you can do import Data.Foo (type X) to explicitly only import the type X without qualification. This proposal is supposed to make -XExplicitNamespaces obsolete, so this is something that should still be possible.

@int-index
Copy link
Contributor Author

Yes, true. I'll try to think of a way to address these issues.

@int-index
Copy link
Contributor Author

@jvanbruegge I addressed both issues.

  1. Instead of Data.BuiltInTypes, introduce Data.BuiltInSyntax, which exports both type constructors and data constructors. Hiding it allows more interesting stuff, like defining strict lists data [] a = !a : ![a].

  2. To translate import Data.Foo (type X), I introduced "import from alias":

    import qualified Data.Foo as {_, T}
    import T(X)
    

    First we import Data.Foo qualified, and then import X unqualified from its alias T.

@jvanbruegge
Copy link

Yes, the changes look really good and I don't have any further comments 👍
Really like the proposal

@int-index
Copy link
Contributor Author

I changed {D, T} to (D, T), but added {D, T} to alternatives. Overall, I don't have a strong preference for any specific syntax, just trying to make it easier on the eyes.

@sheaf
Copy link
Contributor

sheaf commented Jun 6, 2020

I like the proposal, but I find the use of a pair to refer to the term-level/type-level namespaces a bit strange. How about using data as and type as? For imports:

module M where
  import Data.Proxy
    data as D
    type as T

For exports:

module M
  data as D
  type as T
  ( T.Foo ) where

@int-index
Copy link
Contributor Author

int-index commented Jun 6, 2020

@sheaf There are definitely advantages to the data as D type as T syntax, in particular the ease of search (e.g. one can search for "haskell import type as" when they see it for the first time).

But the (D,T) form also has an advantage: it's terse. And I can easily imagine 20+ imports in a Haskell module, where data as ... type as ... will be just noise that does not help readability.

Especially I don't think that splitting a single import into three lines is a good idea, at least based on my experience. Imports already can be a sizable part of a module.

Here's a tool I recently wrote: https://github.com/serokell/hackage-search/blob/fa02183c88cb8fe58da65456d9b774395a5f40ff/backend/Download.hs

Its import section looks like this:

import Prelude hiding (log)
import Control.Concurrent (myThreadId, getNumCapabilities)
import Control.Exception
import Data.Text (Text)
import Data.Foldable
import Data.IORef
import qualified Data.List.Split as List
import qualified Data.Text as Text
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Client.TLS as HTTP
import qualified Network.HTTP.Types as HTTP
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Char8 as ByteString.Char8
import qualified Data.ByteString.Lazy as ByteString.Lazy
import qualified Distribution.Package as Cabal
import qualified Distribution.Pretty as Cabal
import qualified Distribution.PackageDescription.Parsec as Cabal
import qualified Codec.Compression.GZip as GZip
import qualified Codec.Archive.Tar as Tar
import qualified System.Log.FastLogger as Log
import qualified Data.Time.Clock.System as Time
import qualified Data.Time.Clock as Time
import qualified Data.Time.Format as Time
import Data.String (fromString)
import Control.Concurrent.Async (forConcurrently_)
import System.Directory (removeFile)
import System.Exit (die)

Now, say I want to use namespace-specific aliases in those imports. That's what I get with the currently proposed syntax:

import Prelude hiding (log)
import Control.Concurrent (myThreadId, getNumCapabilities)
import Control.Exception as (D, T)
import Data.Dext (Text)
import Data.Foldable as (D, T)
import Data.IORef as (D, T)
import qualified Data.List.Split as (List.D, List.T)
import qualified Data.Dext as (Text.D, Text.T)
import qualified Data.Aeson as (JSON.D, JSON.T)
import qualified Data.Aeson.Dypes as (JSON.D, JSON.T)
import qualified Network.HTTP.Client as (HTTP.D, HTTP.T)
import qualified Network.HTTP.Client.DLS as (HTTP.D, HTTP.T)
import qualified Network.HTTP.Dypes as (HTTP.D, HTTP.T)
import qualified Data.ByteString as (ByteString.D, ByteString.T)
import qualified Data.ByteString.Char8 as (ByteString.Char8.D, ByteString.Char8.T)
import qualified Data.ByteString.Lazy as (ByteString.Lazy.D, ByteString.Lazy.T)
import qualified Distribution.Package as (Cabal.D, Cabal.T)
import qualified Distribution.Pretty as (Cabal.D, Cabal.T)
import qualified Distribution.PackageDescription.Parsec as (Cabal.D, Cabal.T)
import qualified Codec.Compression.GZip as (GZip.D, GZip.T)
import qualified Codec.Archive.Dar as (Tar.D, Tar.T)
import qualified System.Log.FastLogger as (Log.D, Log.T)
import qualified Data.Dime.Clock.System as (Time.D, Time.T)
import qualified Data.Dime.Clock as (Time.D, Time.T)
import qualified Data.Dime.Format as (Time.D, Time.T)
import Data.String (fromString)
import Control.Concurrent.Async (forConcurrently_)
import System.Tirectory (removeFile)
import System.Exit (die)

Not a huge difference. But here's the version with data as ... type as ... syntax, on several lines (as presented in @sheaf's comment):

import Prelude hiding (log)
import Control.Concurrent (myThreadId, getNumCapabilities)
import Control.Exception
  data as D
  type as T
import Data.Dext (Text)
import Data.Foldable
  data as D
  type as T
import Data.IORef
  data as D
  type as T
import qualified Data.List.Split
  data as List.D
  type as List.T
import qualified Data.Dext
  data as Text.D
  type as Text.T
import qualified Data.Aeson
  data as JSON.D
  type as JSON.T
import qualified Data.Aeson.Dypes
  data as JSON.D
  type as JSON.T
import qualified Network.HTTP.Client
  data as HTTP.D
  type as HTTP.T
import qualified Network.HTTP.Client.DLS
  data as HTTP.D
  type as HTTP.T
import qualified Network.HTTP.Dypes
  data as HTTP.D
  type as HTTP.T
import qualified Data.ByteString
  data as ByteString.D
  type as ByteString.T
import qualified Data.ByteString.Char8
  data as ByteString.Char8.D
  type as ByteString.Char8.T
import qualified Data.ByteString.Lazy
  data as ByteString.Lazy.D
  type as ByteString.Lazy.T
import qualified Distribution.Package
  data as Cabal.D
  type as Cabal.T
import qualified Distribution.Pretty
  data as Cabal.D
  type as Cabal.T
import qualified Distribution.PackageDescription.Parsec
  data as Cabal.D
  type as Cabal.T
import qualified Codec.Compression.GZip
  data as GZip.D
  type as GZip.T
import qualified Codec.Archive.Dar
  data as Tar.D
  type as Tar.T
import qualified System.Log.FastLogger
  data as Log.D
  type as Log.T
import qualified Data.Dime.Clock.System
  data as Time.D
  type as Time.T
import qualified Data.Dime.Clock
  data as Time.D
  type as Time.T
import qualified Data.Dime.Format
  data as Time.D
  type as Time.T
import Data.String (fromString)
import Control.Concurrent.Async (forConcurrently_)
import System.Tirectory (removeFile)
import System.Exit (die)

Individually, each import is more readable, but collectively, it ends up less readable (in my opinion) due to excessive repetition.

What about data as ... type as ... on a single line?

import Prelude hiding (log)
import Control.Concurrent (myThreadId, getNumCapabilities)
import Control.Exception data as D type as T
import Data.Dext (Text)
import Data.Foldable data as D type as T
import Data.IORef data as D type as T
import qualified Data.List.Split data as List.D type as List.T
import qualified Data.Dext data as Text.D type as Text.T
import qualified Data.Aeson data as JSON.D type as JSON.T
import qualified Data.Aeson.Dypes data as JSON.D type as JSON.T
import qualified Network.HTTP.Client data as HTTP.D type as HTTP.T
import qualified Network.HTTP.Client.DLS data as HTTP.D type as HTTP.T
import qualified Network.HTTP.Dypes data as HTTP.D type as HTTP.T
import qualified Data.ByteString data as ByteString.D type as ByteString.T
import qualified Data.ByteString.Char8 data as ByteString.Char8.D type as ByteString.Char8.T
import qualified Data.ByteString.Lazy data as ByteString.Lazy.D type as ByteString.Lazy.T
import qualified Distribution.Package data as Cabal.D type as Cabal.T
import qualified Distribution.Pretty data as Cabal.D type as Cabal.T
import qualified Distribution.PackageDescription.Parsec data as Cabal.D type as Cabal.T
import qualified Codec.Compression.GZip data as GZip.D type as GZip.T
import qualified Codec.Archive.Dar data as Tar.D type as Tar.T
import qualified System.Log.FastLogger data as Log.D type as Log.T
import qualified Data.Dime.Clock.System data as Time.D type as Time.T
import qualified Data.Dime.Clock data as Time.D type as Time.T
import qualified Data.Dime.Format data as Time.D type as Time.T
import Data.String (fromString)
import Control.Concurrent.Async (forConcurrently_)
import System.Tirectory (removeFile)
import System.Exit (die)

To me, it looks like keyword soup, i.e. not an improvement over (D, T).

Anyway, that's just my perspective. I'll mention the data as ... type as ... syntax in the alternatives, and I'm open to arguments in its favor.

@goldfirere
Copy link
Contributor

I don't see advantages of this proposal over the relevant parts of #270. I personally find the (D,T) syntax too terse. While I understand your mnemonic of x :: ty, with types later, my instinct is to mention types before terms, as that tends to be dependency order.

I also was hoping that this counterproposal to #270 would aim to be lighter -- but it's not. It's about the same weight (to me).

A concrete downside of this proposal is that is makes module dependency checking harder: import Foo might induce a dependency on Foo, or it might not: it depends on whether Foo is a local alias (which, I assume, would shadow an external module).

The notion of Data.BuiltInSyntax (as opposed to Data.BuiltInTypes) is interesting.

I think what we may need here is to reorganize the soup that has been made of this proposal, #214, and #270. These proposals are all trying to solve the same problem, but they have different moving pieces. Critically, there is some degree of flexibility of which pieces we use. For example, the -Wpuns idea is compatible with the import mechanisms described in any of the proposals. And I think the Data.BuiltInSyntax or Data.BuiltInTypes is also compatible with any of them. It might be nice to have one place that presents the menu, offering combinations that work, somewhat like a chef's menu in a restaurant. Then, we could see the degrees of freedom more easily.

@int-index
Copy link
Contributor Author

I don't see advantages of this proposal over the relevant parts of #270.

It is basically a subset of #270, but the motivation mentions neither punning nor future features, as @simonpj suggested.

The syntax is different, but I currently believe it's better, after the case study I did in #340 (comment)

In actual programs I expect it to result in better readability.

I also was hoping that this counterproposal to #270 would aim to be lighter -- but it's not. It's about the same weight (to me).

To the contrary, I attempted to include most of the same changes but with a different motivation. The one thing left is -Wpuns, which can be proposed alongside VDQ.

somewhat like a chef's menu in a restaurant

I can't really imagine how that document would look like, but if someone was to write it, they can go ahead and copy parts of my text without attribution.

I just put it out there to motivate a subset of #270, and tried to improve minor technicalities while I was at it.

@simonpj
Copy link
Contributor

simonpj commented Jun 8, 2020

It is basically a subset of #270, but the motivation mentions neither punning nor future features, as @simonpj suggested.

Yes, that's the way I understand it. It let's us focus on a single question (how to disambiguate ambiguous references), independent of other concerns. I like that.

There are three bits here:

  1. The idea of allowing distinct "as" aliases for type and term imports.

  2. The idea of providing a way to selectively import all the type or all the term imports. This is the bit where the proposal suggests

    import qualified Data.Proxy as (_, T)
    import T
    
  3. Concrete syntax for the above

Personally I quite like (1) as an alternative to #214. We can argue about (3).

But I cordially dislike (2) -- it's at the root of @rae's objection above. I'm not sure it's essential to the proposal.

@simonpj
Copy link
Contributor

simonpj commented Jun 8, 2020

At the root of (2) is the following question. If a module M exports both a type X and a term X (regardless of whether or not they are related) , how can we selectively import one but not the other:

import M( ???? )

In the body of the module we can use qualified names, by going

import M as (D,T)

and now using D.X or T.X. But we can't do that in the selective import list. Unless, I suppose, we do the obvious thing and allow qualification by D/T:

import M as (D,T) ( T.X )    -- NB T.X in the import list

That looks plausible doesn't it?

@Ericson2314
Copy link
Contributor

Ericson2314 commented Jun 8, 2020

This reminds me of the arguments for having a separate import and (say) open, where one just imports under a name, and the other adds the items under the name to the local scope (#220 was also nudging towards this).

Before, it was hard to notify the motivate, but I think this proposal provides some motivation because:

import qualified Data.Proxy as (_, T)
open T

allows restoring the property that import always makes new dependencies.

@maralorn
Copy link
Contributor

maralorn commented Jun 8, 2020

I really like the distinction between Import and open.
It also mitigates partially the problem that people sometimes wish to import values locally later in the Module. It has been argued that for tooling it is important to have the imports at the beginning of the file. But this doesn't hold for open. So splitting them seems very useful.

This suggested "open" reminds me of "with" in nix or, a better match, "using" in Rust.

Splitting import and open would also play reasonable together with qualified import as default.

@Ericson2314
Copy link
Contributor

#321 is a funny little proposal of mine I don't expect to be wildly popular, but it turns out the module .. as current module synonym idea was first expressed in this comment on this #321 (comment), and so there is in fact great synergy with these two proposals.

Just a small bit of extra motivation----I just hope it doesn't backfire with anyone that thinks #321 is frivolous getting a distaste for this!

@mmhat
Copy link

mmhat commented Jun 20, 2020

Concerning proposed change 4.): Is

import qualified Data.Proxy as (D, T) (D.Proxy)
import qualified Data.Functor as (D, T) (D.Identity)

equivalent to

import qualified Data.Proxy as (D, T)
import qualified Data.Functor as (D, T)
import D (Proxy, Identity)

?

Concerning 7.):
If tuples are no longer builtin but ordinary types there will be a maximum tuple size, right?

@int-index
Copy link
Contributor Author

I've come to dislike the proposed as (T, D) syntax and the "import-from-alias" notion as well. Closing, with the hope that parts of the proposal will be reused in its next iteration (as part of #270 by @Hithroc).

@int-index int-index closed this Sep 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants