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

QualifiedImports #220

Closed
wants to merge 4 commits into from
Closed

Conversation

guibou
Copy link
Contributor

@guibou guibou commented Apr 8, 2019

This proposal adds a new extension to GHC -XQualifiedImports which switches the default behavior of import from importing modules unqualified to importing module qualified. It also introduces a notion of main type of a module import: the main type of a module import is automatically imported unqualified.

This proposal is authored by @aspiwack and @guibou.

Rendered

This proposal adds a new extension to GHC `-XQualifiedImports` which
switches the default behavior of `import` from importing modules
unqualified to importing module qualified. It also introduces a notion of *main type* of a module import: the main type of a module import is automatically imported unqualified.

This proposal is authored by @aspiwack and @guibou.

[Rendered](https://github.com/tweag/ghc-proposals/blob/qualified-import/proposals/0000-default-qualified-import.rst)
@iamrecursion
Copy link

I am strongly in favour of this at a conceptual level, but I'm a touch worried about the recent proliferation of a whole set of extensions to deal with cleaning up import sections. Personally, I'd like to see some explanation of how (or not) -XQualifiedImports might interact with both Module Qualified Syntax and Structured Module Exports/Imports.

With regards to the meat of the proposal itself, I also have a few thoughts:

  • When you talk about the Main Type of a module, I think the principle behind it is excellent. It encapsulates the main meat of our import policy at Luna in a single import statement, rather than the current two we have to use.
import qualified OCI.Data.Graph.Node as Node
import OCI.Data.Graph.Node (Node)
  • When you say 'The qualified-import syntax entry becomes a syntax error', I think a warning might be more appropriate. At least talking from the use-cases I'm familiar with, we'd want to enable it across the entire (large) codebase and want to migrate over a period of time. Making it an error forces a one-shot migration; while this is nicer from a purity perspective, it might not be feasible in larger codebases.
  • While it's a much heavier-weight solution, I find myself drawn to the unqualified keyword in the module export list for the flexibility it provides. I can see it being particularly useful in libraries where a 'main type' and basic set of operators are needed for use. The proposal is right, however, that this brings back some of the issues it's trying to work-around, so on balance I'm not sure it's worth it.
  • Why not, in the case where you don't want to import the main type, just use a hiding declaration? import Data.ByteString as ByteString hiding (ByteString) Is there a problem I'm missing with this approach?
  • How does this proposal suggest dealing with projects that use -XNoImplicitPrelude and manually import their own prelude-esque module? Just forcing import MyPrelude unqualified in all cases?

Overall, I definitely understand the motivation behind this proposal and like it a lot!

@aspiwack
Copy link
Contributor

aspiwack commented Apr 9, 2019

@iamrecursion thanks for the very detailed comment. @guibou is away most of the week, so in the meantime, let me try to fill the gap.

Personally, I'd like to see some explanation of how (or not) -XQualifiedImports might interact with both Module Qualified Syntax and Structured Module Exports/Imports.

I don't expect any interaction (after all, what we are proposing is, at the end of the day, just syntax). But I'd rather wait until these proposals are accepted before making a detailed comparison.

Why not, in the case where you don't want to import the main type, just use a hiding declaration? import Data.ByteString as ByteString hiding (ByteString) Is there a problem I'm missing with this approach?

With the current semantics of hiding, it would mean that ByteString would not be imported at all. If you really want ByteString.ByteString in the environment but not ByteString, then you are out of luck without some mechanism to deactivate the implicit unqualified import. At this point, we don't believe it's really a problem and justifies more syntax. But we'd be happy to be proved wrong.

How does this proposal suggest dealing with projects that use -XNoImplicitPrelude and manually import their own prelude-esque module? Just forcing import MyPrelude unqualified in all cases?

Indeed. And, to be honest, I fail to see what an alternative could be. The spirit of this proposal is that it's fine to import a module unqualified: it just needs to be a conscious decision.

@iamrecursion
Copy link

I don't expect any interaction (after all, what we are proposing is, at the end of the day, just syntax). But I'd rather wait until these proposals are accepted before making a detailed comparison.

Entirely fair. My main concern with the overlap is that they all involve syntactic changes to the import statements, and with GHC already suffering from a proliferation of language extensions, there's some not-insignificant potential for confusion.

With the current semantics of hiding, it would mean that ByteString would not be imported at all.

That's a very good point. My comment was more idle speculation than a proposal for a concrete use-case. I certainly can't see a need for it at the moment.

Indeed. And, to be honest, I fail to see what an alternative could be. The spirit of this proposal is that it's fine to import a module unqualified: it just needs to be a conscious decision.

That's absolutely what I expected! Perfect!

@simonpj
Copy link
Contributor

simonpj commented Apr 15, 2019

I can see your motivation, but wonder if it's really worth all this just to change the default.

Also I forsee that people will indeed want to nominate more than one "main type" or function.

Also, with the "main type" thing, we can still get name clashes. So we need a a way to make the main type be impoted qualified, which adds more complexity.

So, based on a very superficial look, I'm unconvinced that the benefits justify the costs. That said, the complexity doesn't ramify: it's a well-localised change.

@Ericson2314
Copy link
Contributor

Part of my reservation i'm not sure what namespaces we ought to have in Haskell. From the dependent Haskell front, we have too many already. Rust also makes some interesting arguments about name-spacing fields, constructors, and maybe even arbitrary functions (most controversially of all) under a type. ("Inherent impls" make no sense in theory, but certainly do solve the Map.function problem in practice.)

Given all the uncertainty in my mind, I just rather see how thinks shake out. I rather solve the more "tech debt" issues like import Module quailified as ... in the meantime, which clear the clutter to make thinking easier while not prematurely shaking up the foundation.

@guibou
Copy link
Contributor Author

guibou commented Apr 23, 2019

Thank you for your answer @simonpj

I can see your motivation, but wonder if it's really worth all this just to change the default.

Changing the default is the core of this proposal, we are convinced that it will lead to cleaner import behaviors. The "main type" import is just a syntax sugar on top. We can abandon the "main type" part of the proposal, but not changing the default means abandoning the proposal.

Also I forsee that people will indeed want to nominate more than one "main type" or function.

Yes, we also noticed that in the alternative section. The current state of our proposal comes with two properties:

  • The name of the main type can be deduced from the name of the imported module. I.e import Data.ByteString as ByteString will import main type Data.ByteString.ByteString as ByteString.
  • It matchs a common pattern in the community in common libraries, such as bytestring, vector, containers, ...

Nominating many "main type" won't preserve these properties, especially their explicitness. If someone want to import unqualified many types or functions, she can explicitly do so using the standard way:

import Data.Foo unqualified (MainType1, MainType2)

I'm really in favor of explicit behavior, or principle of least surprise. "Main type" is a surprise, but with really limited impact and it follows a pattern already used in the community.

Also, with the "main type" thing, we can still get name clashes. So we need a a way to make the main type be impoted qualified, which adds more complexity.

I'm convinced that the name clashes risk in this context is really limited. On the other hand, we can see this conflict as positive because they will create a dependency between the module namespace and the type / function namespace. For example, using the following import:

import Data.ByteString as ByteString

With our proposal, this means:

  • qualified import of the Data.ByteString module as ByteString
  • unqualified import of the Data.ByteString.ByteString type as ByteString.

In this context, the ByteString name is used for both the module and a type. In my point of view, it feels sane, because both are related.

Now, we can imagine a conflict if the ByteString type is imported unqualified using "main type" and another ByteString symbol is in conflict.

I can think of three examples of such a conflict:

  • If the conflicting ByteString symbol is defined in the same module:

    import Data.ByteString as ByteString
    ...
    data ByteString = ... -- my ByteString implementation

    From my point of view, this case is an example of bad naming scheme in a project, you won't name a type the same as a module you just imported.

  • If the conflicting ByteString symbol is defined in another module which is itself imported unqualified:

    import Data.ByteString as ByteString
    import AnotherModule unqualified

    In this case, AnotherModule exports a ByteString symbol which will conflict. I'm not considering this case as an issue, because any symbol defined in AnotherModule may generate a conflict with anything else, not just ByteString. That's the reason why we are writing this proposal, we want to reduce the number of unqualified imports.

  • If two modules are imported with the same binding name and are both containing a type with the same name:

    import Data.ByteString as ByteString
    import Data.ByteString.Lazy as ByteString

    Will conflict because the ByteString type exists in both modules. First of all, I think I had never observed this pattern. Second point, the conflict already exists: ByteString.ByteString is ambiguous and our proposal will just show this conflict for the symbol ByteString too.

I cannot think of any other source of name conflict with the "main type" feature.

But, anyway, if you want to disable the import of the main type, just export the module with a different name:

import Data.ByteString as LegacyByteString
data ByteString = ... -- my bytestring, no conflict with LegacyByteString.ByteString

@simonmar
Copy link

I'm very unkeen on the idea of treating some identifiers differently in a module (the "main type" thing). It feels very much like an ad-hoc hack and bakes in a particular convention that happens to have been adopted for some (but not all) popular data structure libraries. Modifying the language to better support one particular convention doesn't seem right.

If we want to do something more general here, I would like to make it possible for a module to export qualified identifiers. Then Data.ByteString could decide to export ByteString unqualified and everything else qualified, if it wants. Then at the import site we should have the option of importing the hierarchy, and possibly renaming parts of it. (of course this is a vague description of something that would be very complex to flesh out fully and has a large design space, but if we're talking about what direction we should go in, I think this is it.)

@jhenahan
Copy link

jhenahan commented May 2, 2019

Bikeshedding: Why not open import or import open? If qualified (closed) imports will be default, it feels annoying to carry around an 11 character keyword where it's no longer necessary. I'm also not wildly into moving qualification to the end of the import. End of the day, I think what I'd like is

module MyModule where

open import Prelude
open import My.Library
open import Data.Map (Map)
open import Data.Text (Text)

import Data.Map as Map
import Data.Text as T

It's now obvious where unqualified names can come from, and all those fancy import formatters can manage this cleanly. By moving qualification to the end, tools (and users!) have to make it to the end of the import before they know how the import is resolved. With the above, I can scan imports and know immediately how this works.

Stealing a word like open doesn't seem ideal, though, so there may be a nicer word for it.

@llelf
Copy link

llelf commented May 2, 2019

@guibou My gut feeling is that a lot more people would be against “main” type (very confusing) than to “qualified by default” (clear enough what it does and how).

@recursion-ninja
Copy link

recursion-ninja commented May 2, 2019

@jhenahan, If you're not a fan of "stealing" open why not re-use forall:

import forall Prelude
import Data.Map as Map

@akhra
Copy link

akhra commented May 3, 2019

Another option which steals nothing:

import Prelude as default

@rwe
Copy link

rwe commented May 10, 2019

First, I like the idea of this extension, insofar as making import statements default qualified. Generally I feel that Haskell's import system being unhygienic by default is a mistake, and it would be nice to flip that bit at a project level.

Split out "main type" proposal

The "main type" thing should be split into a different proposal. I agree with @simonmar that it feels like a hack. If one were to attempt to solve that issue in a less ad-hoc way, I'd expect it to be something like exposing modules as records, taking inspiration from ML or even ES6. Though that's far from a trivial thing and shouldn't be coupled with an otherwise simple change.

open keyword

The open keyword seems like the right choice, and I believe wouldn't cause conflicts. forall has an entirely different meaning (both as a keyword and in common lexicon), and I think recycling an existing for a different purpose and semantics would make things more confusing.

One note about that though is that I'd expect it to be positioned wherever qualified would have gone. So definitely not open import Foo. Instead either import open Foo, or import Foo open if using the postfix extension (#190).

I believe in that position, it prevents any syntactic ambiguity with e.g. clashing identifiers with modules that expose an open symbol.

@int-index
Copy link
Contributor

int-index commented May 10, 2019

I agree with @erydo about using open. It is the keyword that Agda uses, and since qualified is a pseudo-keyword, it is not a big deal to support another one in the same position. We are not stealing any syntax!

@guibou
Copy link
Contributor Author

guibou commented May 14, 2019

Thank you for your comments.

I do understand the arguments against the main type and I'll move it out of the proposal (i.e. in the Alternative section).

About the open versus unqualified, that's an interesting discussion. I'll edit the proposal with open as a keyword (and move unqualified in the altenative section) and follow @eyrdo suggestion of having it at the place of the qualified keyword, with respect to the usage of the postfix extension.

About @tejon suggestion, to use import Foo as default, I'm not in favor of this suggestion because it breaks the current pattern of importing unqualified with an optional qualified name (i.e. current syntax import Foo as Bar will expose all Foo symbol unqualified and also in the qualified Bar module). This pattern is used when you want to import unqualified, but with a module name alias to fix ambiguities.

Thank you, I'll update the proposal with theses changes soon (i.e. in a few days, I'm unfortunately rather busy theses days).

Based from the proposal discussion:

- The main type may be part of a separate proposal
- `open` is used instead of `unqualified`.
@guibou
Copy link
Contributor Author

guibou commented Jul 3, 2019

Could we send this to the committee? Discussion seems to have converged.

@aspiwack
Copy link
Contributor

aspiwack commented Jul 3, 2019

@nomeata ⬆️

@nomeata nomeata added the Pending shepherd recommendation The shepherd needs to evaluate the proposal and make a recommendataion label Jul 3, 2019
@simonmar
Copy link

simonmar commented Jul 4, 2019

Committee shepherd here.

(firstly noting that most of the discussion thread on the proposal is irrelevant, as it refers to earlier iterations of the proposal, which has been heavily modified since.)

My inclination is to reject the proposal. As per our committee process I'll put the justification below, and we can discuss.

The change itself is relatively innocuous: an extension that switches the default from importing unqualified to qualified, and a new syntax import M open to override the default. Users don't have to enable the extension, but those that wanted it could use it.

However, the underlying theme here is not one of mechanism, but one of policy. The proposal argues that it would be better if Haskell authors in general switch to a style that uses qualified imports routinely. The proposed extension would both facilitate and encourage this policy.

I don't think it's clear at all that using a mostly-qualified style would be better.

  • First, let's be clear that we're never going to qualify everything. Nobody is going to write a Prelude.+ b. So it's mostly-unqualified vs. mostly-qualified.
  • In a mostly-qualified world, most imports would look like import Data.HashMap.Strict as HashMap, because qualifying things like Data.HashMap.Strict.insert is too much of a mouthful. So it's not as if qualifying by default really solves the name-clash problem, because we're already shortening the module names for convenience. So users get an additional obligation, namely to choose a suitable short-name for every import. People would choose different short-names, and code becomes less readable overall. This is already a problem with Data.Text, where some people do import qualified Data.Text as T and others do import qualified Data.Text as Text, and so on.
  • On balance I don't think the current situation (mostly-unqualified) is bad at all. The language has good ways to control namespacing, and in general I think it's good to have the choice between designing an API for qualified import vs. unqualified import - I tend to use both styles in practice.

For better or worse, there's significant momentum behind the current style, and even if we were to decide that it would be better to use a different style, we'll be stuck in a world with both styles essentially indefinitely - I don't want to think how many 3-release cycles it would take to change the base package APIs at this point and how much needless breakage it would entail.

@simonpj
Copy link
Contributor

simonpj commented Jul 4, 2019

Were this proposal to be adopted, I feel strongly that it should affect only the default. Regardless of the setting of the default, you should be able to specify what you want:

import A qualified      -- Qualified, like it says
import B unqualified    --  Unqualified, like it says
import C               -- Uses the default, whatever that is

At that point you can write code that will work regardless of the default, simply by being explicit. I like that.

And then, is it worth having a flag to control the default, when you don't explicitly say "qualified" or "unqualified"? Maybe so. This feels very much in the same category as the post-postive qualified idea: it makes a difference to some, and other things being equal that's clearly a good thing.

Finally, what is the default-default; that is, the default when you specify no language extension. Here it's harder to argue for a change, because it'd invalidate all existing code. And the proposal does not make such a case; absent an extension flag, the existing behaviour remains unchanged.

@simonmar
Copy link

simonmar commented Jul 4, 2019

I feel strongly that it should affect only the default

Indeed, that is the case. The proposal chooses open rather than unqualified (but suggests the latter as an alternative).

This feels very much in the same category as the post-postive qualified idea: it makes a difference to some, and other things being equal that's clearly a good thing.

It's similar to the QualifedPost proposal in a literal sense - it's just a syntactic change - but it feels different, because this proprosal's underlying motivation is the suggestion that we should change how we design and use APIs. And hence, by accepting this proposal into GHC we would be facilitating (and arguably endorsing) that policy change. So it seems like the right time to make an editorial decision on the policy, not just the mechanism.

If we decide the policy is bad, should we nevertheless accept the mechanism, for the benefit of those who feel differently? I don't think so - the existence of the mechanism itself would lead to some fragmentation of the ecosystem into those who want to use this style and those who don't, and that's arguably not a good thing.

If it were clear that the community really wanted to move in this direction then I think it would be different, but I'm not seeing that level of support.

@simonpj
Copy link
Contributor

simonpj commented Jul 4, 2019

Indeed, that is the case.

I don't think so. The proposal explicitly says that import qualified M is rejected if you switch on -XQualifiedImports. But that choice is easily changed, and IMHO should be.

by accepting this proposal into GHC we would be facilitating (and arguably endorsing) that policy change.

I dont't think so. It is not changing anything unless you switch on the flag. And if you switch on the flag, it just changes what happens when you say import A, leaving unaffected what happens if you say import A qualified or import A unqualified.

It looks very opt-in to me, rather than changing how we design and use APIs.

@goldfirere
Copy link
Contributor

I wholeheartedly agree with the sentiment. And it's the essence of this proposal too: qualified import optimise for reading, while unqualified imports optimise for writing.

I'd argue that the very slight loss in readability of the import list is negligible in comparison. After all, the import list is not really the part of the a file that one spends time reading: we spend most of our time in the actual program.

Hm. That's a good point. I'm now right in the middle. Maybe leaning ever so slightly in favor.

@akhra
Copy link

akhra commented Jul 7, 2019

Taking that a step farther, I don't think this module affects reading a module at all except for the (local, obvious) open marker. Consider the following:

module Foo where

import Data.Text
import Data.ByteString as B

foo :: Data.Text.Text -> B.ByteString
foo = B.encodeUtf8

This module is legal with or without the proposed extension. It might seem like weird style, but everything is legal and no meanings are changed. Until and unless you want to edit it, there is exactly zero cognitive load here. Adding open to an import line tells you right there that something is different (so it doesn't matter if the extension is project-wide), and still won't cause the rest of the module to contain anything that would have a different meaning if the extension was off and open was nonexistent.

In fact, if we switch from a pseudo-keyword to an inline {-# OPEN #-} pragma, any module written with this extension on would be 100% legal with it off!

@simonmar
Copy link

simonmar commented Jul 8, 2019

Update on the status and points made so far:

The proposal is an opt-in extension. This is true of course - but it's also true of all extension proposals. We should consider whether adding this extension is a net benefit.

My personal view is that we should accept extensions if we think they have a reasonable chance of making it into a future language standard. This extension is quite speculative - the authors argue in favour of a different style, which this extension would facilitate, and in order to find out whether the style catches on, we need to add the extension and see what happens.

If the style doesn't catch on, then having the extension at all would be a net negative - extra complexity, documentation, and more chances for people to get confused.

Do we believe the style will catch on? I'm sceptical - in addition to the points I raised earlier, based on working with a lot of Haskell programmers over the years and seeing what styles people prefer, generally it seems we prefer to find a good balance between qualifying things (which optimises for first-time reading of code in an unfamiliar codebase) and not qualifying (which optimises for working with the code in a familiar codebase, because the code is less cluttered).

Also I'm just not seeing a whole lot of support from the community for this proposal. This matches what I've seen elsewhere - there are a few people who strongly believe in qualifying as much as possible, whereas the majority are happy with the status quo. I think the minority are already well supported by explicit qualified imports.

The qualified-by-default style optimises for reading over writing. I think this could be argued either way - I find lots of qualified names make the code look very cluttered, which makes code harder to read by people who are familiar with the local "vocabulary". Yes it optimises for first-time reading of code in an unfamiliar codebase, but I'm not sure that's the case we want to optimise for.

@simonpj
Copy link
Contributor

simonpj commented Jul 8, 2019

in order to find out whether the style catches on, we need to add the extension and see what happens.... If the style doesn't catch on, then having the extension at all would be a net negative

That is true enough. But it's worth remembering that Haskell programmers are a diverse bunch. It's entirely possible for a style to catch on with one subgroup, who really appreciate it, and not with another. And Haskell has lots of "two ways to do things": let vs where, case vs pattern matching definitions, etc.

I'm not making a strong argument for this particular extension; just saying that for me, when it comes to low-complexity syntactic variations, it's enough that a substantial constituency is strongly in favour. I would not want an extension advocated by a single person; but I would be influenced by knowing that there were lots of production users who want it.

Our difficulty is that a committee discussion is good for eliciting technical clarity, but not well suited to discovering if such a constituency exists among users. I wonder if it would be worth thinking about such a mechanism? I'm not quite sure what. At the moment all we have is "how many people made supportive noises in the discussion thread", something that is complicated by the fact that (in general) proposals often morph a bit during discussion.

Syntax is always hard.

@glaebhoerl
Copy link

My personal view is that we should accept extensions if we think they have a reasonable chance of making it into a future language standard.

Of course, there's no requirement to continue following a precedent if one believes it was misguided, but this proposal feels most similar to Strict and StrictData to me, which are in a different spirit.

@gridaphobe
Copy link
Contributor

I'm not particularly convinced by the optimized for reading-vs-writing argument either, for precisely @simonmar's reason. Excessive qualification makes code difficult to read, and the question of "where is this function defined" seems much better answered by good tooling in my opinion.

But this is not the primary motivation of the proposal. The proposal wants to address the issue of maintaining import/hiding lists, which certainly can become a pain. One alternative not mentioned by the proposal, which I have gravitated to, is to discourage the use of import/hiding lists. Instead, either import the entire module unqualified, or qualify it. This is not a rigid rule for me, but it seems to work quite well as a rule of thumb, and most of my imports end up unqualified. To do this successfully though, you need to know which modules really need to be imported qualified. Some are well-known to any seasoned Haskeller (text, bytestring, the myriad containers, etc.). For other libraries, one thing I've noticed authors doing is specifying in the haddocks that the module is intended to be imported qualified, which is very helpful.

So I think that the underlying issue this proposal wants to address could be successfully tackled by the community without changes to the language. That being said, I find it hard to argue against adding the option to change the default, as it's a small, stylistic change with a small maintenance burden for GHC. I've always appreciated Haskell's large source language that accommodates differing styles, and this very much feels like a decision that different teams could reasonably disagree on.

@simonmar
Copy link

simonmar commented Jul 9, 2019

Out of curiosity I looked at a couple of large production Haskell projects at work (over 100k import lines in total) and found

  • ~20% of imports were qualified (~75% of those use as)
  • ~10% of imports had import lists
  • ~2% of imports used hiding

Caveats apply of course - there is a local style guide that encourages a certain style, although it doesn't say anything about qualified imports.

Import lists are an unsustainable drag in practice, so we mostly don't use them. In GHC we came to the same conclusion several years ago: originally GHC used import lists everywhere, but it became just too painful, so we removed most of them. Life has been much easier since.

Developers generally seem to prefer to use unqualifed imports until they encounter a name clash, and then they use either qualified or an import list to resolve it. Name clashes are resolved lazily. In our case, developers do have tooling to resolve names (but not everywhere, and I don't think they all use it).

For GHC the figures are slightly different:

  • ~3% of imports are qualified (~80% of those use as)
  • ~20% of imports use import lists
  • ~2% use hiding

I think this reflects a preference to use import lists over qualification to resolve name clashes, and perhaps some inertia from the time when we used import lists everywhere.

@rwe
Copy link

rwe commented Jul 10, 2019

There's a quantitative reason (versus qualitative/stylistic "easier to read") for why explicit or qualified imports are valuable to a reader, and could theoretically be helpful to tooling, and so are worth encouraging.

Basically, by being able to determine locally and syntactically which import introduced a symbol, you reduce the complexity of symbol search down from "search the entire module graph" to "follow the linear path of imports until you get to the definition".

Currently, we take for granted that tools crawl the entire module dependency tree before being able to provide any symbol information. Under this view, the difference between qualified and unqualified symbol use seems merely aesthetic.

However, as a human, I've very rarely read the entire import tree. When diving into an unfamiliar project, explicit or qualified imports help me so much, because I can see exactly where to look for the symbol definition. This is especially helpful when there are external libraries: grep in the codebase fails, hoogle gives me too many results, and frankly most of the "real" tooling like ghc-mod/hie/etc. can be flakey or slow; and in any case doesn't apply to non-building environments (code reviews, GitHub, "just looking", etc). So that's why I find them beneficial as a reader.

Tooling can also benefit when explicit/qualified imports are used by prioritizing which modules to crawl or build when first opening a file and receiving a symbol request, before finishing loading the entire project. For example, if you open a file with a dozen imports, and ask what foo is; if it can locally determine which module that comes from, it could return a result much faster (even if it's just a syntactic "it's defined here").

As an aside, explicit exports have a similar benefit, from the other direction of the search: If you can look at a module syntactically and see that it does not export a particular symbol, then you don't have to consider its imports when looking for that symbol. (Or, at least limit the search to re-exported modules). Helpful both as a reader, and potentially to tooling in the middle of loading module information.

It's true that there's an overhead to being explicit; it's way easier to just throw up your hands and "import everything from everything and figure out what I mean when you compile it". But there's also more than pedagogical value alone to qualification or explicit imports. It isn't an all-or-nothing tradeoff, either—it applies to each symbol used in that way.

Given that reading happens under many conditions, and writing usually happens in the context of a functioning and installed development environment, my opinion is the balance of value might tend to lean toward "optimize for reading".

(I also think that as tooling improves, those import lists might be better automatically managed, so that developers can code as though using open imports, but have a beautifier unfold or qualify them. Best of both worlds; developers shouldn't have to deal too much with those kinds of style choices).

@akhra
Copy link

akhra commented Jul 11, 2019

Basically, by being able to determine locally and syntactically which import introduced a symbol, you reduce the complexity of symbol search down from "search the entire module graph" to "follow the linear path of imports until you get to the definition".

Massive +1 to this; it's the perfect distillation of how I understand "optimize for reading" in this context, but there are likely other interpretations and I'm glad it occurred to @erydo to spell it out so clearly!

@simonmar
Copy link

Ok, let's see where we are with this proposal.

I have personally argued against it, for reasons explained in more detail above, but which I'll summarise briefly:

  • Every extension, even a simple syntactic one, has costs - of which the most important in my view is the potential for creating new opportunities for confusion.
  • For an extension to be worth adding, the benefits have to outweigh the cost. I don't believe the benefits in this case meet the bar.
  • Separately, I don't actually believe that "qualified by default" is either a good idea, or what the majority of Haskell programmers actually want.

I think to accept this proposal we would need some evidence that there is a substantial cohort of users who would want it, or a reasonable chance that it would catch on.

@simonpj said above:

I'm not making a strong argument for this particular extension; just saying that for me, when it comes to low-complexity syntactic variations, it's enough that a substantial constituency is strongly in favour. I would not want an extension advocated by a single person; but I would be influenced by knowing that there were lots of production users who want it.

Our difficulty is that a committee discussion is good for eliciting technical clarity, but not well suited to discovering if such a constituency exists among users. I wonder if it would be worth thinking about such a mechanism? I'm not quite sure what. At the moment all we have is "how many people made supportive noises in the discussion thread", something that is complicated by the fact that (in general) proposals often morph a bit during discussion.

I gave some data above that suggests, at least in some large production codebases, we don't see a tendency towards more qualification. The other data point is the reactions on the proposal (currently 5+ 6-). Do we have any other data here?

@int-index
Copy link
Contributor

Do we have any other data here?

I'd like to contribute a data point.

I used to work on a large Haskell codebase (100k+ lines of code) where the style guide required to use either qualified imports or explicit import lists. People typically used explicit import lists rather than qualified imports. This supports @simonmar point that

in some large production codebases, we don't see a tendency towards more qualification.

In practice, it ended up a very painful experience, as almost every merge/rebase ended up with numerous conflicts in the import section. It was such a major time sink that I felt like an import section engineer rather than a software engineer.

My conclusion is that explicit import lists are not a viable code style, at least as long as there is no tooling with custom merge strategies for import sections.

A much better workflow is to use unique identifiers project-wide, prefer unqualified imports, and set up jump-to-definition (fast-tags works rather nicely).


This said, the proposal argues for use of qualified imports specifically, not explicit import lists. I've noticed that the two are mixed together in the discussion! Let's not mix them up: I don't think qualified imports would entail the same issues that I experienced with explicit import lists.

@simonpj
Copy link
Contributor

simonpj commented Jul 16, 2019

Dear author, @guibou, you have not responded to the recent conversation. How are you feeling about the proposal now? There may be changes you want to make. For example, I strongly recommend that import qualified M remains legal even with your extension on, for resaons I describe above.

I note that we accepted a proposal in very similar territory, simply moving where the qualified keyword appears. It has a very similar cost/benefit to this one: a minor syntactic change to accommodate the porgramming style of a particular sub-group. It seems a bit inconsistent to accept one and reject the other.

As to support, in this thread apart from @guibou himself, I'm not sure I see anyone else in strong support. (@erydo and @tejon argue for "optimise for reading" but I'm uncertain whether or not they are arguing for this proposal.) The disucssion is a bit confused by the fact that the proposal was originally more elaborate, with a "main type", but was then simplified.

We are in committee-review mode now, which makes it harder to gather such feedback.

I'm firmly on the fence. If it were just me, I wouldn't do it. But if a significant (even if small) constituency wanted it, I'd be happy to accommodate it. @guibou do you have any evidence that there are others who want this, or is it just a good idea you had and thought was worth discussion?

@simonmar
Copy link

I note that we accepted a proposal in very similar territory, simply moving where the qualified keyword appears. It has a very similar cost/benefit to this one: a minor syntactic change to accommodate the porgramming style of a particular sub-group. It seems a bit inconsistent to accept one and reject the other.

I think there's actually a big difference between the two proposals. ImportQualifedPost doesn't change the meaning of existing syntax, whereas QualifiedImports does. If you turn it on, you get different scoping rules! This increases the potential for confusion a lot - someone modifying a module that uses QualifiedImports might be confused that they can't refer to unqualified identifiers if they don't know about the extension.

@aspiwack
Copy link
Contributor

@int-index

This said, the proposal argues for use of qualified imports specifically, not explicit import lists. I've noticed that the two are mixed together in the discussion! Let's not mix them up: I don't think qualified imports would entail the same issues that I experienced with explicit import lists.

For the records: it's very much not about import lists. Rather, it's about avoiding import lists (in part because of the phenomenon you describe).

@rwe
Copy link

rwe commented Jul 17, 2019

(@erydo and @tejon argue for "optimise for reading" but I'm uncertain whether or not they are arguing for this proposal.)

@simonpj — To be explicit, my argument is in favor of the proposal. At least, the spirit of it. I believe it encourages a "better" default semantics for imports.

My only remaining nit was the minor syntactic comment which might have been overlooked or just not made it into the text:

[…] I'd expect open to be positioned wherever qualified would have gone. […] either import open Foo, or import Foo open if using the postfix extension (#190).

Regarding @simonmar and others' concerns about confusion (which are worth considering), my thinking is:

  • Any module with the open keyword in its imports must be using this extension, unambiguously.
  • Any module which compiles using this extension that does not use the open keyword, would be interpreted the same as if this extension was turned off. (Because it must be either qualifying its symbol use or using import lists). So that's unconfusing also.
  • When modifying code written with this extension — I suspect the surrounding code would necessarily clue the developer in as per above. But if their confusion from a "variable not in scope" error is still a concern, perhaps the verbose form of that error might note that the extension was in effect?

@simonpj
Copy link
Contributor

simonpj commented Jul 17, 2019

(@erydo and @tejon argue for "optimise for reading" but I'm uncertain whether or not they are arguing for this proposal.)

@tejon was kind enough to respond privately (because it's under committee review) to me, with permission to share:

"Apologies for not being explicit: I support this proposal and would immediately argue to enable it globally in my company's codebase (and I don't anticipate resistance on that). It is likely that the majority of our imports would still be open, but I share @guibou's conviction that this should be explicit. Polluting the top-level namespace, and withholding breadcrumbs to an identifier's provenance, should be conscious decisions.

"I'm also very much in harmony with everyone decrying the pains of maintaining import lists! Qualified imports have a completely different profile there, which is why my company's style has evolved to favor them in general. For a data point: roughly 25% of our current imports are qualified; only 10% have an import list, mostly for operators."

That's useful information, thanks. I think that if any others would like to speak up briefly in support, we'd be glad to hear from you, on-thread. I don't want to start a new for-and-against debate, just gather info on whether there are others thinking "oh yes, if this flag existed I'd use it a lot -- even across my company".

@guibou
Copy link
Contributor Author

guibou commented Jul 17, 2019

Dear author, @guibou, you have not responded to the recent conversation. How are you feeling about the proposal now? There may be changes you want to make. For example, I strongly recommend that import qualified M remains legal even with your extension on, for resaons I describe above.

I do agree, it should remain legal. We initially wanted to forbid import qualified M to reduce the number of valid import statements.

However, if the qualified keyword stays, the unqualified keyword may be a better naming than the open keyword for unqualified imports, just for symmetry.

@tejon gives a really interesting comment (#220 (comment)). He observed that "qualified import style" is actually working without the qualified by default. Turning on the extension just makes this programming style more robust.

I'm firmly on the fence. If it were just me, I wouldn't do it. But if a significant (even if small) constituency wanted it, I'd be happy to accommodate it. @guibou do you have any evidence that there are others who want this, or is it just a good idea you had and thought was worth discussion?

I have no evidences unfortunately. All started from a discussion between me and @aspiwack where we realized that we were agreeing on our interest for qualified by default.

I have an observation however. The qualified by default style is Python's default. However one may argue that qualified by default is a style which is not surprising for python developer coming to Haskell and I've heard a few people coming from python and surprised by the import semantic in Haskell.

@erydo commented about the syntax:

My only remaining nit was the minor syntactic comment which might have been overlooked or just not made it into the text:
[…] I'd expect open to be positioned wherever qualified would have gone. […] either import open Foo, or import Foo open if using the postfix extension (#190).

I totally agree with that and I apologize if the current proposal text is unclear about that matter. open (or unqualified) should appears in the place of qualified.

As a personal experience, I'm working on a 100K+ lines of Haskell codebase. 8% of it are import lines (probably even more, I just counted import, but some imports may span multiples lines). 10% of them are qualified import, 80% are explicit import list. That's a nightmare to maintain and everyday I fell as an "import section engineer" (Thank @int-index for the expression). A research in this codebase history shows that most import started as unqualified without import list and slowly evolved as unqualified with import list / hiding to handle conflicts. Switching to qualified import by default will surely reduce the size of our import headers at the cost of more qualifiers in the middle of the code. From my point of view that's better for readability.

Considering the current state of the discussion, I'm motivated to change the proposal as such:

  • qualified remains valid
  • the unqualified keyword is introduced instead of open, for symetry with qualified. The position is the same as the one of qualified.
  • the (No)QualifiedImports LANGUAGE pragma just change the default.

Thank you.

@aspiwack
Copy link
Contributor

I have an observation however. The qualified by default style is Python's default.

So is Ocaml's.

I totally agree with that and I apologize if the current proposal text is unclear about that matter. open (or unqualified) should appears in the place of qualified.

I would be perfectly ok if -XQualifiedImport implied -XImportQualifiedPost. As it would reduce the number of possible combinations. But either way flies.

@simonmar
Copy link

I have an observation however. The qualified by default style is Python's default.

So is Ocaml's.

True, however in Python it works in conjunction with the convention of terse module names and less use of a hierarchy. random vs. System.Random, string vs. Data.String, calendar vs. Data.Time.Calendar, etc.

So in Haskell to use this style we'd be using as a lot, which is OK, but not ideal - different people will use different conventions for module aliases.

And let's not forget that you can already do this today! If there are people who believe strongly in this style of importing, there should be example of it in the wild, no? Those who feel strongly wouldn't be put off by the need to write one extra keyword per import, I would think.

@simonmar
Copy link

Not to let this linger too long, let's try to arrive at a decision here.

I'll propose that we reject the proposal.

It's clear that there's a section of the community that feels strongly that imported identifiers should be qualified by default. I don't think that's an unreasonable position - indeed as has been pointed out, other languages successfully take this approach. I'd like to thank the authors for the proposal and the enlightening discussion it has generated.

Extensions that only a subset of the community would be happy to enable should incur a much higher level of scrutiny, because this essentially introduces a fork in the GHC language. Some modules would enable the extension and some wouldn't, so Haskell programmers have to be aware of both variants of the language, increasing complexity and steepening the learning curve.

We might adopt an extension of this kind if it was clear that we (as a community) wanted to migrate in a particular direction over time, but in the case of this proposal I don't think that's the case. We have a section of the community who feel strongly, but no indication that this section is large or growing.

Any more thoughts before we close the proposal?

@simonpj
Copy link
Contributor

simonpj commented Sep 16, 2019

As I say above, I don't feel strongly either way, I'm content with Simon's proposal to reject.

The only issue is one of defaults, not of exrpessiveness. I see no global consensus that that qualified import is a better default. And locally, one can always establish a coding convention that you should always import qualified.

@bravit
Copy link
Contributor

bravit commented Sep 16, 2019

While I like the proposal and feel that it's a good direction to move that way, I agree with the @simonmar's reasoning and support rejection.

@guibou
Copy link
Contributor Author

guibou commented Sep 16, 2019

I do understand your motivation for the rejection.

I thank you all for the time you took to review and discuss this proposal. Hopefully I'll have more success in my future proposals.

@simonpj
Copy link
Contributor

simonpj commented Sep 16, 2019

@guibou, please don't regard this as "failure" and acceptance as "success". Everything in language design is a balance, and a judgement call. Not much is right or wrong, black or white. We make progress by trying things, sometimes drawing back, and trying again. (e.g. I'm on my fourth attempt at finding a decent way to add impredicative polymorphism to Haskell.) Every exploration means that we understand the space a bit better.

Thank you for contributing to that exploration.

@simonmar simonmar added Rejected The committee has decided to reject the proposal and removed Pending shepherd recommendation The shepherd needs to evaluate the proposal and make a recommendataion labels Oct 24, 2019
@simonmar simonmar closed this Oct 24, 2019
@aspiwack aspiwack mentioned this pull request Jul 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Rejected The committee has decided to reject the proposal
Development

Successfully merging this pull request may close these issues.

None yet