[Pre-Pitch] Import access control: a modest proposal

Yes.

That’s it. That’s the post.

9 Likes

+1

How do you see this intersecting with @_spi in the future?

7 Likes

I approve of any removal of underscore special cases.

But I feel like there are use cases for fileprivate, and open. To me @_exported import feels like an adjustment of visibility of inheritance so it lines up w/ open. public seems quite on par w/ what we have today of normal imports, and internal would be VERY nice to have as @_implementationOnly being that the scope of the internal declaration there is to the module itself. But there are times at which I want to control the scope of types such that they do not leak through implementations and a file private restriction would be welcomed in my book either spelled as fileprivate import or private import ether way feels like something that would be helpful for framework development (especially for those of us who need to work on things that do not expose internal bits to the outside world from our frameworks when it comes to layering or types).

10 Likes

This seems great! I'm super excited to see imports getting some love! I like the reuse of public and internal as a way to make the behavior seem a bit more familiar for folks since they're already used to those keywords.

I think it would be a great time to fix this if it wouldn't be asking too much, assuming that folks could pretty easily recover from that, and they would have to have opted in to Swift 6 mode anyways. There's a big thread on potential less major solutions to this but I haven't gotten a chance to actually work on any of them Supporting strict imports

I think so too, from our attempts to use @ _implementationOnly in a modularized project for the same reasons you described this specific issue meant we can't use them nearly anywhere in our project because almost every module is included for some type you want to store at some point.

I think it's still worth considering a warning for this case because one thing I could see happening is a developer not realizing that another file in the library they were working on already importing something, and then their new public import foo not working as they expect, and they'd have to end up knowing about the fact that the internal import is preferred to understand they need to go search for other uses and add public to them.

8 Likes

I like the direction you’ve outlined here.

There’s also another import-related feature I’d like to see, which Jordan touched on in the post you linked:

Suppose I write a library which vends a protocol. My library has no dependency requirements, but there are types in, say, Foundation and Numerics and Collections which could all conform to my protocol.

Currently, I have to make separate shim modules like “MyLibraryFoundationShim”, “MyLibraryNumericsShim”, and so forth, to provide the conformances, so that clients which don’t use those other libraries can still use mine.

But then clients of my library which do use those other libraries must remember to include the corresponding shims as well. This makes it a hassle for clients to use my library, since they need to import multiple dependencies instead of just one.

• • •

It would be much more ergonomic if my library could make a “convenience import” of those other libraries.

Then the conformances of, say, Numerics types to my protocol would be available for clients who import both my library and Numerics, but other clients who don’t import Numerics would still be able to use my library without any problems.

7 Likes

I like the public/internal distinction and agree that internal should be (or should have been) the default.

But it's been painful to watch the Java ecosystem struggle to migrate to the privacy of modules. One unmigrated library can foul many dependency trees.

In Swift, even applications may (should?) be composed of multiple modules, and need migrating. Applications targeting multiple OS platforms and versions might delay upgrading to a new language default of internal until all target platforms support it (a big lift if concurrency is not available on older platforms). This in turn stalls apps on old versions of libraries whose next version requires internal support. (Many Java libraries cite their clients when balking at converting to modules.)

I also like jordan rose on @_exported. Could this belong in package-level visibility control, where module/library publishers opt-in to internal?

E.g., let's say library L-1 in Package.swift can declare a dependency on a module L-0 to be internal, resulting in compiler errors when any L-0 types/names are used in the public API/ABI of L-1. That way, each library can opt in (or not) to using internal with their next version, and library clients manage around it at the same time they manage library version upgrades (i.e., they can remain on old versions if needed). And the new library itself can be used in older versions of Swift.

The scope change from per-import to per-module-dependency avoids the incongruity of multiple imports with different access levels and avoids the need for new access modifier on the import statement. But the cost is complexity: the full public API/ABI of module L-0 is visible to clients of L-1.

But that might be right. I'm a little uncomfortable with library/module L-1 selectively exposing types from module L-0 to application clients. That would mean there is a different view of L-0 for each L-n publishing L-0 types to app A (or for each import statement?). If the publisher of L-0 presents the only/fixed view of L-0, it might help incremental compilation and developer reasoning (at the cost of a larger API/ABI surface).

I'm not sure if this helps or hurts de-conflicting multiple internal dependencies on (different versions of) L-0. I'm prefer that happen early at package-declaration phase, rather than at link-time via discovery.

2 Likes

Wouldn't it be more in keeping with the current semantics (and the plans to support @_exported in the future) if

  • @_implementationOnly import translated to private import,
  • the meaning of import stayed the same as in Swift 5.5 (but potentially able to be also expressed as internal import); and
  • public import became a candidate for a potential, future equivalent to @_exported import)?
3 Likes

I like your proposal, it canonicalizes something that‘s already widely used with the private @_ wrappers.

Is that not served by the feature known as “cross-importing”?

6 Likes

I think the primary virtue of what’s proposed here is that it addresses the shortcoming that the current meaning of import isn’t internal to the module at all. For this reason I am quite fond of the plan as pitched by @beccadax. Any alternative that involves keeping the meaning of import as-is in today’s Swift maintains source compatibility but then also the principal problem that needs fixing.

2 Likes

By my reading of the pitch, this would be entirely a compile-time change and platform support would not be an issue. I may be misunderstanding though.

1 Like

I don't think so. This basically treats private, internal and public as meaning "lowest access", "default access", and "highest access", rather than as meaning "visible in this file", "visible in this module", and "visible in any module". It would be strange that you could use a type from a private import in an internal var, or one from an internal import in a public var.

If we do have something called private import, it should probably do what @Philippe_Hausler suggests upthread: only allow you to visibly use the type in private/fileprivate declarations, so the import is effectively an implementation detail encapsulated in that one file.

3 Likes

I see two sides to the question of integrating this with @_spi:

  1. The current @_spi(X) import M attribute adds to the import to allow access to the SPI group X, it doesn't affect the visibility of the import itself like the other import attributes discussed here. Because of this difference it could remain a distinct attribute that can be combined with the proposed public import and internal import. I don’t think we want to change its semantic but instead clarify it and maybe find a better attribute or syntax.
  2. What is more interesting is a new kind of visibility level for imports that we are considering to make a dependency visible at the same level as @_spi decls. This, let’s say, @_spiOnly public import M would allow the use of the APIs and SPIs of M in the local SPIs and be printed only in the private textual interface. All clients of the module would then load M iff they have access to the private interface, this would impose the same project or organization boundary from the current @_spi implementation. The syntax is still very much up for discussion and it could be better integrated with what the current pitch brings. I’d be interested to hear any ideas about this.

I'm a big fan of this, I often create files which encapsulate something I need to pull in from Foundation or another library (and expose an internal API instead). Having compiler support for this would be a +1.

Would it be possible to mix visibility of imports? For example:

public import struct Foundation.URL
import class Foundation.FileManager
6 Likes

This was a big pain point with using @_implementationOnly for me, so I'm really happy you want to fix this. :+1:

2 Likes

+1 to this pitch, although I'd love to see these exceptions (i.e. bugs) resolved.

I don't think clients should see extension, operators, etc for a module they do not directly import (unless the the module is explicitly exported like @Philippe_Hausler mentioned).

7 Likes

I quite like this proposal in general, so I’m +1, but I should note that this change does inflict most of its breakage on those who were not using the features we are adopting. That is, while it’s a strict quality of life improvement for those using @_implementationOnly imports, it will break almost all existing Swift frameworks today.

While @beccadax rightly calls out that these breakages should be easily fixed, I do just want to note that it does affect essentially all Swift code, even if they weren’t using the feature.

2 Likes

This sounds great! The way Swift’s import statements "leak" has always felt very unintuitive to me, and very inconsistent with the rest of the language design.

Like @George, I would also love the ability to use private import to import things as implementation details without leaking them to the whole module. Perhaps including that in the proposal would help make the change more palatable to framework authors who now have to change their code? "Yes you have to change all your imports, but you also get this new tool for scoping imports inside your module."

2 Likes

I love the direction this is going :clap::+1:! I have one question, however:

Why not use open import SomeKit to achieve the same result as the current @_exported import SomeKit? It feels to me like open being a more pervasive version of public when it comes to classes (namely, being public except with the added ability to subclass/override) would fit well with the imports, where (namely, open being public except with the added ability to re-expose the imported content as if it was defined locally). It even sounds right: "open import", as in "an import that is also open to the client code". Regarding the argument that current @_exported import occupies a very niche role and is not used too often, I can see a similarity with classes in this regard as well: Most classes are final because if their reference semantics (and all that it implies) and classes that are designed to be overridable (e.g. non-final) are much more rare, because the same effect can be achieved in most cases much better by using protocols (hence, the official guideline of marking all new classes as final unless an explicit need for subclassing arises). So, just like seeing open class is rare and indicates an unusual case, open import would indicate an unusual case for a very similar set of reasons.

1 Like

Do we need special keywords here? Can the compiler inspect the API and automatically export those modules exposed publicly and keep those implementation-only which are not?