chapel-lang / chapel

a Productive Parallel Programming Language
https://chapel-lang.org
Other
1.79k stars 421 forks source link

Discussion: How to re-export symbols in module hierarchy? #13979

Open BryantLam opened 5 years ago

BryantLam commented 5 years ago

Re-exporting is when a module makes available symbols defined in another module as if they were defined in that module.


Design discussion. Subthread under #13831 and #13978.

Motivation: Re-exports are used to expose a public interface that is different than the internal module hierarchy.

What is the behavior expected from re-exports? Is it sufficient to attach this behavior to public import and public use?

public module M {
  public proc fn() { writeln("fn"); }
}

module A {
  public import M only fn;
}

module B {
  public use M only fn;
}

/* Case 1 */ {
  import A;
  A.M.fn();
}
/* Case 2 */ {
  use A;
  M.fn();
}
/* Case 3 */ {
  import B;
  B.fn(); // M is hidden via facade
}
/* Case 4 */ {
  use B;
  fn();
}

While subject to change, these candidate behaviors seem reasonable to me because use and import statements would have their own intended use cases.


One concern I have is that if qualified access from use statements is not removed in #13978, re-exports could become confusing to learn because all public use locations would also have the qualified access re-exported as if a public import occurred too. For Case 4, it could be confusing that M.fn() would also work.

Though, one alternative path is to check if private import is used and prevent the qualified access: https://github.com/chapel-lang/chapel/issues/13528#issuecomment-526140095. This approach would work, but it also feels like an unfortunate consequence that use statements also have qualified access because now the visibility of the module symbol is tied to a special case / precedence order of private import and public use.


Another approach is to consider the suggestions from Revisiting modules, take 3: make import statements only do imports and have some new reexport keyword to make desired symbols visible to other modules. The same could be applied to use statements.

Privacy specifiers on import/use statements are no longer necessary and it would be a clean separation of functionality because re-exporting wouldn't be tied to use or import statements anymore, ... but it also feels unnecessary if public import and public use are sufficient, especially with "learnability" precedence in Rust.

lydia-duncan commented 5 years ago

One concern I have is that if qualified access from use statements is not removed in #13978, re-exports could become confusing to learn because all public use locations would also have the qualified access re-exported as if a public import occurred too. For Case 4, it could be confusing that M.fn() would also work.

Would limiting re-export of qualified naming to only when an explicit import is present be less confusing? E.g. for a set of modules defined like this

module A {
  var x: int;
}
module B {
  use A;
}
module C {
  import A;
}

would it be more or less confusing if C.x worked but B.x didn't?

I view the enabling of qualified access through use statements as a secondary concern, especially once import statements are implemented. While I like having it part of use statements for convenience, I wouldn't object to limiting it to only enabling qualified access in the scope in which it is defined, if that made it more palatable.

I think I would be okay with a reexport symbol in principle - I prefer having an explicit indicator that a symbol will be accessible via a different qualified naming path rather than having it be an additional property of something that is more focused on the current scope, though I am a little leery of adding so many keywords.

mppf commented 5 years ago

I wouldn't object to limiting it to only enabling qualified access in the scope in which it is defined

So the example would be this, right?

module A {
  var x: int;
  // A.x available because of current-module-name rule
}
module B {
  use A;
  // A.x potentially available here because of new "used symbol in scope defined" rule
}
module C {
  import A;
  // A.x is always available here
}
module BB {
  use B;
  // A.x is not available here
  // B.x is available here
}
module CC {
  import C;
  // C.A.x is available here
}

If that's the case, I'd consider adopting it. We could describe it as "A use statement always creates a private symbol referring to the module symbol itself to enable qualified access. The module symbol is not exported to other modules even if the use statement is public. In contrast, a public import statement will create a public symbol referring to the imported module, so that if module M contains (public) import A then a user/importer of M could access A itself.

Somehow I didn't follow this part:

Would limiting re-export of qualified naming to only when an explicit import is present be less confusing would it be more or less confusing if C.x worked but B.x didn't?

Is there something wrong with the example? I might be just getting confused because it doesn't say where we are considering C.x or B.x working. I'm hoping that my variant on the example above seems like an obvious restatement of what you were saying...

lydia-duncan commented 5 years ago

Yeah, that's the right interpretation, thanks!

mppf commented 4 years ago

Continuing this comment - https://github.com/chapel-lang/chapel/issues/14407#issuecomment-582548651 - if a method is private (which we don't have yet, see #6067) - then I don't think it should be possible to use a re-export to make that method public again. The reason is that the method exists in a nested namespace (say the class on which it is defined) and since use / import will control namespaces/what is in scope, but not what methods are available, it won't make sense to allow re-export of the methods that case.

Of course re-export could be used to control visibility of the type with the methods.

I think this is O.K. because I think the purpose of re-exporting is to control namespace creep rather than to enforce public/private.