SE-0386: `package` access modifier

This idea has been discussed before and I agree it's important. It's actually already implemented as an underscored attribute (@_implementationOnly import).

I don't think that package private modules can address the same needs as the package modifier. Being required to group every interface that ought to be private to the package into separate modules is an awkward restriction on the organization of code and it doesn't work for a common case like this:

// PublicType is declared in a "public" module in your package
public struct PublicType {
  // This field is an implementation detail needed by other modules in the
  // package but is not meant for use outside the package
  package var x: Int 

  // ...
}

An extension in a package-private module is not a substitute for this since the extension cannot declare storage or implement a wrapper for x without x being public.

Since a lot of the discussion is revolving around whether we should consider solving these problems with @_spi instead, I'd like to elaborate on one of my posts from the pitch thread:

It is an important goal for me and many others in this discussion that the second and third rows be addressed distinctly. For package implementers there's an important advantage to being confident that you are free to evolve package visibility declarations as needed, without having to worry about outside dependencies you have little control over. When refactoring code in a package, encountering the package access modifier should give you a warm fuzzy feeling: "phew, this declaration is probably not going to be a straight jacket that limits my ability to make necessary changes." Encountering declarations with @_spi, on the other hand, yields a different kind of reaction: "Darn, I'm going to need to do research to figure out how complicated it is to change this and who I need to coordinate with in order to do so."

Folks have rightfully pointed out that we could design an official version of @_spi that is a tool for both jobs either by teaching the tooling which SPI groups are package private or by adding extra arguments to the attribute. I do understand the sentiment that these concepts are close enough to each other that maybe we could get a two-for-one by designing a shared solution, but I haven't seen an argument that convinces me that combining them is a superior design for the specific need addressed by package. One thing in particular that bothers me about reusing the @_spi approach is that I think the ceremony of declaring named groupings does not pull its weight for package private declarations because most of the time you're just going to declare one name for them. I think that if you address all the problems with reusing @_spi (tooling needs to be aware what's package private, group names are superfluous, naming @_spi at the import declaration is questionable ceremony for this purpose) then you wind up back where we started with a different spelling of the same thing that is being proposed.

I'm coming around to this perspective. Although it's a mouthful I do wonder if we should name this access level something like packageprivate since a package declaration does look like it is declaring an object which is a package.

4 Likes

i don’t think that “modules in the same project” is a useful intended audience, because i often have example targets (or, for packages that require 5.7+, snippet targets) that i don’t want to be included in that audience.

i wouldn’t want code in an example target or an integration test target that compiles in the CI pipeline, but fails to compile when used in an actual project.

8 Likes

My argument is that for a large/and or old enough project (or any number of other cases, as @taylorswift points out) this will not be true—to the point I would say that package is the thing that's too permissive, in contrast to a formalized @spi mechanism.


An addendum to my earlier thoughts: Part of my concern is that in the proposal as stated import MyModule and import MyModule mean two different things based on criteria that aren't visible at the call site. I don't see how this is meaningfully different from async without the await keyword, or Swift's error model without try—in fact it's worse, because with those at least I can "jump to definition" to immediately understand the context. With package I have to read and internalize the on-disk structure of the project (which will be changing over time) in order to understand the consequences of any import in the project (or read the entirety of the documentation? This is maybe beyond the scope of this particular thread, but "interaction with DocC" seems like it should be added as a bullet to proposals in the future).

I would move from a strong -1 to an "indifferent" if the proposal were amended to require some explicit indication of intent at the use site, but at that point it seems pretty clear this is "just" a special form of @spi.

2 Likes

This seems pretty easily solved simply by including some premade SPIs, as I mentioned up thread. Not only would it provide uniformity among all packages for certain levels of visibility, but the SPI approach is far more flexible, as it already allows custom naming. Continually customizing Swift's ACLs for this sort of visibility doesn't seem to scale. A full SPI solution not only provides more flexibility immediately, but more room to grow premade identifiers if additional scopes become common. To me, providing a premade Package SPI, as well as things like Testing or custom identifiers with custom visibility, all seem like distinct advantages for the SPI approach that ACLs really can't match.

5 Likes

This is an interesting point I hadn't considered that's worth spelling out in the proposal, I think:

Currently, code in a separate Example module within the same package is guaranteed to work in the same way when someone else copies the examples into their standalone, separate package. The @testable attribute allows the importing code to poke a hole into this invariant, not from within the library itself.

With the proposed package attribute, this invariant will no longer hold: @taylorswift would have to move his examples into a separate package to be assured of this.

Mind you, when it comes to the issue of "work[s] the same way" that I mention above, it's more than just that some code might no longer compile when moved outside the package because it calls some package-internal API. There is also the much more difficult-to-reason-about scenario where the code continues to compile but ends up calling a different overload.

Consider also that library authors who avoid exposing public extensions of standard library or Foundation types will (correctly) feel that it's fine to write package-internal extensions, and think of the head scratching that would ensue when an end user copies some example code from that library into their own code only to find a difference in the behavior of a standard library type due to overload resolution discrepancies.

5 Likes

-1

I hadn't previously looked into what exactly @_spi does, but based on the description of it here in the alternatives considered section it's exactly the tool I've wanted for providing a private API to other modules, while package is a significantly less flexible tool that requires contorting your packaging to do things like not have access to privates from another module. Having to explicitly request the SPI by name is basically the same thing as having to use @testable import, and I'm a fan of that as well as it's good to have some indication when reading some code that an import isn't doing the normal thing. It sounds like Apple internally uses @_spi for only very specific and limited scenarios, but that doesn't mean that the feature itself is only suitable for that.

2 Likes

Another idea just had is the SPIs could be expanded with a list of allowed target names at declaration time. This would allow even more granular control than package.

i think we need a place to put the @spi definition, and this is something Package.swift should handle. because we already have to edit Package.swift when you rename a module.

For a long time I thought I wanted a package ACL. But then I read this little gem of a sentence.

I have a lot of code in many packages whose sole purpose is to exercise the API as though from the outside to prove as a compilation test that no public modifiers are missing and any future refactors do not lose them. (This is a lesson learned from depending on other packages such as SwiftPM itself who often refactor and release an update that cannot be used without a patch to re‐expose the shuffled pieces.)

If I had not read @taylorswift’s comment, I suspect I would have jumped on using the package ACL for a few months, eventually run into trouble and then realized that I had undermined all my related tests. At that point I would likely abandon the package ACL again in favour of an equivalent @spi (if it weren’t underscored) for the sake of the notation at the import site. With the help of foresight I think I will avoid that churn.

To convince me to use it now, the proposal would have to add some way of carving out part of the package from participating in the ACL so as to make that portion compile as though it were a client.

7 Likes

Perhaps swift pm could allow one to explicitly override the package-name for a given target - then it would fundamentally solve this.

Strong +1. Too many times I had to use @_spi as a workaround, and I consider it inadequate in most use cases it had to be applied. I also find that suggestions to continue using @_spi miss the point that it's an underscored attribute that can go away in the next release of the compiler with no warning. Its behavior is clearly different from the proposed package modifier. Suggesting that @_spi should be formalized and introduced separately is totally fair, but the lack of non-underscored @spi in the language shouldn't block the introduction of package.

Yes, it resolves a long-standing issue, which led to leaking certain symbols in a package that previously had to be made public or use underscored attributes.

Yes, it's consistent with most of the existing access modifiers (except fileprivate) in naming and fits quite nicely. I don't find packageprivate alternative to be appealing, as it is too verbose. I'd rather see fileprivate being renamed to something shorter than have one more inconsistent access modifier introduced.

If private were declarationprivate and public were modulepublic, then certainly packageprivate would make sense. Otherwise, when compared to simple package it adds verbosity with little to no benefit.

I have only used languages with much more simpler sets of access modifiers, and I'm glad that what Swift has is more advanced and flexible. With addition of package it becomes even more polished.

I followed development of package access modifier from the pitch phase and had a look at implementation PRs on the SwiftPM repository.

1 Like

In our case we define feature modules, that is, targets that contain the code for a feature of the app, but in several case we would like to split that target into separate "sub-targets", one for each screen of the feature for example, for namespacing and visibility purposes within the target itself. Currently, we use caseless enum-based namespaces for that, but it's not great.

If this proposal is approved, we could hypothetically move the grouping one level higher and define "feature packages" that contain modules that pertain to specific aspects of the feature, but adding lots of packages for that would probably me hard to manage and unproductive.

+0

From the example in proposal, most of them can use swiftmoule.private to solve. Different module in the same package can search the private swiftmodule and import the types which don't want to expose. And it works just like C-language which use private headers to seperate the caller from outside(means caller we don't know) or "Package"(means, our frameworks)

More about that the "Subclassing and Overrides" part. The matrix list there, makes me a little hard to remember and realize for the massive combination. Is "Subclassable" and "Accessible/Callable" the same semantic and we have to treat it as top level control ? Because previous is Nx2, now is Nx3 (N means accessible level). Only one package keyword can not hold these extra control information.

Seems from your site, "Subclassble" should be a additive standalone attribute on "public/internal" or "package". Any real world use case to make some APIs "Accessible outside package but subclassable inside package" and can not use the workaround we have...


I mean, if we really want this precise control mentioned above, things will become much more complicated...

A little joke, if we can re-design Swift's language at the beginning (I know it's impossible...Just a joke :slightly_smiling_face:

Seperate the "access level" and "subclass level".

// 1. Defaults non-subclassing for class. Remove "final" and introduce "subclassing" keyword (or "nonfinal" keyword, whatever
// 2. Remove "open" keyword, which act the same as "public subclassing(public)" or just "public subclassing"
// 3. "package/public/internal/fileprivate subclassing" without brackets means "package/public/internal/fileprivate subclassing(package/public/internal/fileprivate)". The private does not support subclassing combination. The first attribute must be higher than or equal to the attribute inside subclassing brackets.

public subclassing class Foobar {} // Same as below
public subclassing(public) class Foobar {} // access in anyone, subclassable anyone
public subclassing(package) class Foobar {} // access in anyone, only subclassable inside package
public subclassing(internal) class Foobar {} // access in anyone, only subclassable inside module
public subclassing(fileprivate) class Foobar {} // access in anyone, only subclassable inside same file

package subclassing class Foobar {} // Same as below
package subclassing(package) class Foobar {} // access in package, subclassable inside package
package subclassing(internal) class Foobar {} // access in package, only subclassable inside module
package subclassing(fileprivate) class Foobar {} // access in package, only subclassable inside same file

internal subclassing class Foobar {} // Same as below
internal subclassing(internal) class Foobar {} // access in module, subclassable inside module
internal subclassing(fileprivate) class Foobar {} // access in module, only subclassable inside same file

fileprivate subclassing class Foobar {} // Same as below
fileprivate subclassing(fileprivate) class Foobar {} // access in same file, subclassable inside same file

About the proposal's Future Discussion which can not use private swiftmodule:

reducing the need for explicit module aliases when utility modules of different packages share a name (such as Utility) or when multiple versions of a package need to be built into the same program.

Is this private swiftmodule naming do cause conflict ? For example, Vapor.Utility is not even visible to your MyLib package or "MyLib.Utility" module. The issue only happends if you explicit access their private module in swift compiler options.

I don't think it's good usage of this feature to access others' private swiftmodule. For this case, @spi is better way to workaround.

I'm having trouble understanding your argument across these two posts, because you're talking about this Private module in packages as if it were an existing feature of the language and SwiftPM. Is the right way to understand you that you're describing how you'd like that feature to work and that you think it would be a better alternative to what's in the proposal?

I don't think anyone is suggesting this

My fault. I used to think that private.swiftinterface is another feature, after checking the compiler guide I found it's current @_spi usage

But anyway, can we put this public/private swiftinterface/swiftmodule design more further ? Like a standard to use this to distribute different interfaces to differfent consumer (Package level or Anyone)

I'd like to both bump this review thread — I'm sure more people have thoughts on this — and specifically ask for feedback about @usableFromPackageInline. There was a suggestion earlier to rename this to @usableFromInline(package). If we did accept the package modifier in general, what do people think about these two spellings, or is there an alternative that you would prefer?

1 Like

i do not like the existing @usableFromInline, because i feel like it should not really be an attribute, it is closer to being an access level, because i often fall into the following pattern of encapsulation:

public > @usableFromInline > internal, fileprivate, private, etc.

so in everyday programming @usableFromInline is a weird kind of “halfpublic” that exists to make @inlinable and @frozen errors go away. but that is not really what @usableFromInline actually does, because @usableFromInline API is actually public API, with the restriction that only the swift compiler is able to call that API.

so from a package author standpoint, @usableFromInline is effectively public and we have to treat those declarations as if they are public, because anyone can call them, we are only guaranteed that no humans will be calling them.

and if you really think about it, all roads here lead back to @_spi, because i think the best way of understanding @usableFromInline is to imagine it as

@_spi(compiler) public

where compiler is an SPI that only the swift compiler can import.

so this hypothetical @usableFromPackageInline is really just a product of two SPIs, like

@_spi(compiler, packagelocal) public

where the caller has to be eligible to import both SPIs in order to use the declaration.

so i am -1 on @usableFromPackageInline.

3 Likes