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

Remove qualified access from use statements? #13978

Closed
BryantLam opened this issue Sep 4, 2019 · 7 comments
Closed

Remove qualified access from use statements? #13978

BryantLam opened this issue Sep 4, 2019 · 7 comments

Comments

@BryantLam
Copy link

BryantLam commented Sep 4, 2019

Should use statements enable qualified access? E.g.

module Lib {
  var someVariable = 1;
}
module LibUser {
  use Lib;
}
module Main {
  use LibUser;
  var x = Lib.someVariable; // Lib.someVariable is qualified access
}

Note that there is some support for allowing use Lib to allow qualified access within the module containing that statement (so later on in module LibUser in the above example there could be an expression like Lib.someVariable).

Reasons to keep qualified access for use:

  • it would be a breaking change and it relies on import existing
  • qualified access is still available for resolving naming conflict between modules in the common case that modules are used

Reasons to remove qualified access for use:

  • avoids some unintended problems from qualified + unqualified access in the same scope
  • it separates concerns between use and import to allow for greater control of symbol visibility
    • public use Lib within M would hide the fact that symbols come from Lib (so M.Lib will not be available)
    • public import Lib within M would expose Lib within M (so M.Lib will be available)
    • This is nice because making import public or private controls the scope of the module imported; while making a use public or private only affects the contents of the module used.

Forked from #13119 (comment) and #11262 (comment). Also, this topic is the first of the Open Questions from #13831.

Today, Chapel's use statement does three things AFAICT:

  1. unqualified access to a module's symbols in current scope
  2. qualified access to the symbol in current scope
  3. if public use in current scope and current scope is a module declaration, a dependent scope can inherit behaviors 1 and 2 through another use statement

Having both qualified and unqualified accesses being allowed by one statement causes some unintended problems: #11262, #13925, #13925 (comment)

So it got me thinking, how often would it be that a user actually wants both qualified and unqualified access to a symbol? Rarely. It would be better to separate these behaviors as separate statements.

This proposal is to remove qualified access on use statements.

Caveat: use statements would only do unqualified access, so users that prefer qualified access need import statements #13119 #13831 to be implemented before this proposal is accepted.

One idiom today is use only to allow only qualified access:

use M only;
use M except *;

Under this proposal, these statements effectively become no-ops. This would be a breaking change and the best long-term course is to introduce a compiler error to recommend using import instead.

(It could be a warning, but I don't see how warnings that allow no-op behaviors are useful when the user is likely coming from outdated information or is confused, so their code will likely break later anyway.)

@lydia-duncan
Copy link
Member

lydia-duncan commented Sep 18, 2019

One argument in favor of maintaining the current behavior - it allows more ways to resolve conflicts when two used modules provide the same symbol, and is the easiest strategy available. E.g.

module M {
  var x = 10;
  ... // Other important symbols
}
module N {
  var x = 11;
  ... // Other important symbols
}
module User {
  proc main() {
    use M, N;
    writeln(x);  // This will error due to x being defined in both M and N
  }
}

Today, there are a few ways to resolve this error. I'm going to list them in order of ease-of-use, to emphasize my point.

Strategy 1: Use qualified naming, made available through the use statement itself

This is by far the easiest option for the user. They know which version they meant to refer to, just add an explicit prefix and you're good to go. If you only utilize the symbol in one place, or use both versions in a small handful of places, this is probably the solution to go with.

use M, N;
writeln(M.x);

Strategy 2: Exclude x from one of the modules

This involves splitting the use statement onto two lines, because limitation clauses are only allowed to contain a single module today. This is more appropriate if you find yourself using x over and over again and only need one version - then the cost of adding the prefix to the symbol becomes greater than the cost of splitting the use statement and hiding the version you don't want.

// If you wanted N's x
use M except x;
use N;
writeln(x);
// If you wanted M's x
use M;
use N except x;
writeln(x);

Strategy 3: Rename one of the versions being brought into scope

This is more annoying than the previous strategy, but appropriate when you want both versions frequently. Since limitation clauses are only allowed to contain a single module today, and you probably wanted to reference the other symbols in M and N, you need to have one use statement to exclude the version you are renaming, and one use statement to rename it.

// If you wanted to rename M's x
use M except x;
use M only x as y;
use N;
writeln(x);
writeln(y);
// If you wanted to rename N's x
use M;
use N except x;
use N only x as y;
writeln(x);
writeln(y);

How would this work with imports?

Strategies 2 and 3 would obviously still be available, unchanged. Strategy 1 now could also be accomplished with an additional statement:

Strategy 1b:

use M, N;
import M;
writeln(M.x);

The only time strategy 1b would be used if strategy 1 was still available would be if the user wants to be explicit about enabling the qualified naming. I don't know of any instance where a user has written use M only; or use M except *; when an unlimited use was already present, which would be equivalent. Personally, I wouldn't ever even think to utilize any variant of strategy 1b when strategy 1 was available.

In a world where use statements no longer enable qualified access to a symbol, I would rank this strategy as easier than strategy 3, but similarly as easy as strategy 2 (since it requires both adding a line and modifying the reference that was causing problems).

Personally, I suspect this means strategy 2 would be my go-to solution, but that may be biased by living in a world where previously use could accomplish both (so I would probably find having to write an additional import annoying, at least for a little while).

Impact

Removing qualified naming support from use statements removes the easiest strategy for resolving a naming conflict. Other strategies for resolving the conflict are still available, but require more effort on the user's part than the strategy that is getting removed. These strategies are already the better choice when the naming conflict occurs more frequently in the scope, but for a simple single conflict, the strategy that this proposal would remove was preferred.

@mppf
Copy link
Member

mppf commented Sep 19, 2019

@lydia-duncan - yeah, but if we have import statements, I would have expected users to write

import M, N;
writeln(M.x);

and if the wanted to have unqualified access to symbols within,

import * from M, N;
writeln(M.x);

(or something along those lines).

It's my expectation that we're trying to encourage people to use import and qualified access more. As a result I don't see the change to the ease of use of use in this case to be a big deal.

@lydia-duncan
Copy link
Member

I did anticipate that being a response. However, I don't think it is a fair one - I view the addition of import statements as providing an alternative strategy rather than one that should fully replace use statements. While I do agree that we want to encourage qualified naming as a way to avoid polluting scopes, I don't think that means we should hobble the utility of use statements as part of that encouragement. If a user truly wants the symbols of another module to be accessible as though they were defined in the current module, wouldn't making it harder to do that fully (by forcing them to use separate statements to accomplish what would have been possible in a single one) be as likely to discourage qualified naming as it is to discourage unqualified naming? Especially since unqualified naming is "easier" since there's less typing involved?

@mppf
Copy link
Member

mppf commented Sep 19, 2019

I don't think that means we should hobble the utility of use statements

I think that makes it sound worse than it is, but really we're arguing about "Which is more useful / less confusing" which is subjective.

If a user truly wants the symbols of another module to be accessible as though they were defined in the current module, wouldn't making it harder to do that fully ...

Say we are talking about use M;. If the symbols in M were cut & pasted into the current module, M would not exist, so I don't see the argument here.

Over on #13979 (and specifically #13979 (comment) ) I think you suggested allowing use statements to bring in the module name, but treat that module name as private (even for a public use). That proposal would be fine with me.

@lydia-duncan
Copy link
Member

If the symbols in M were cut & pasted into the current module, M would not exist, so I don't see the argument here.

Yeah, I'm definitely more following the re-export case

@bradcray
Copy link
Member

Catching up: My preference here is to create a world in which either use or import is sufficient for expressing all namespace-related patterns (or stating it the other way: to avoid a world in which the only way to get every pattern is to mix use and import). As I understand it, this issue's proposal would require use to be used for some patterns and import for others.

My proposal for having a pattern for use that disables qualified access would be to extend the proposal in issue #10799 to support a form of renaming the module so that it couldn't be referenced. The first version of this that occurred to me was use M as _; / use M as _ only x; ("use M as a name that isn't a legal identifier using a symbol that's already used to drop names on the floor."). A slightly less clear alternative would be use M as ; / use M as _ only x;. I'm open to other placeholders for the underscore or omitted identifier in this approach, but haven't come up with an alternative I prefer yet (but also haven't tried particularly hard).

This is obviously a little clunky-looking, making the proposed variants of an import statement that doesn't bring things in by default probably more appropriate and attractive for programmers with a "don't make symbols available by default" mindset. But despite the clunkiness, it meets my criteria of making use sufficient for such patterns, being consistent with other language features, and keeping use true to its "make everything visible by default" roots (while avoiding the need for changes to existing code).

@BryantLam
Copy link
Author

I agree with Brad that I would prefer a world where import statements replaced use statements and that use statements could be implemented as a combination of import statements for the sake of consistency.

If I were to design an import statement, my inspiration would be Rust's choice of syntax to not import [module identifier] but instead go the route of import [item identifier] where item could be a module identifier, some variable identifier in a module, a package identifier, etc. That way, the operation being performed (rename, re-export) is unambiguous as it is being performed on some item identifier.

For this issue, use statements (qualified + unqualified access) could be equivalent to:

import M; // qualified access
import M.*; // unqualified access; bring in everything in M, but not M

and features like renaming may apply only to one or both of these import statements to generate the equivalent use statement.

Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants