Hello, Swift community.
The review of SE-0386: package
access modifier ran from January 26th through February 8th. There was a lot of feedback in the review, and some of the issues are subtle. The Language Workgroup spent a long time trying to decide what to do, which is why this announcement is a bit late.
Review feedback was mixed about the proposal. Many people wrote to say that they were very happy with the proposal; many people also expressed concerns about it, ranging from the design concepts to the surface syntax. One of the questions we always ask in a review is whether the problem being addressed is significant enough to warrant a change to Swift. The feedback was fairly clear on that, at least: there is definitely a real need for giving related modules access to declarations without going so far as to make them public
. The discussion is whether this proposal is the best way to achieve that.
Specificity of control and the @_spi
feature
Some community members felt that the proposal's approach was insufficiently general. package
as proposed allows indiscriminate access within a single group of related modules. However, there are use cases where it's helpful to be more judicious about the users of an API, discouraging access outside of its intended clients. There's also fair reason to be concerned about allowing only a single level of grouping above the module level; maybe some people will want a hierarchy of such groups. Swift already has an unofficial feature that allows some cross-module access, @_spi
, and most of these community members would like us to embrace and generalize that rather than just adding a new access level.
The Language Workgroup believes that adding a new access level is the right thing to do.
Imagine you're writing an API to which this feature might apply. You don't want to simply make it public
, so you must have concerns about it being used by untrusted clients. One way you could protect it would be to enumerate all the clients you trust with it. To do that, you'd need a way to specify all of those clients, maybe even as narrowly as a specific function. You'd also have to update that list every time you found a reason for new code to use the API. That might be a little onerous sometimes, but it'd always be very clear which clients were allowed to use it.
In principle, a programming language's entire access control system could be designed that way: every declaration is either public
, private
, or has an exact list of the entities allowed to use it. No doubt, some people would find that design abstractly appealing. But it's interesting to note that Swift already doesn't take that approach within a module. If you want a declaration to be usable elsewhere in a file, you can make it fileprivate
; if you want it to be usable elsewhere in a module, you can make it internal
; but you can't be more precise than that. This is a deliberate decision, and it is based on an assumption about what it means to group some code into a module.
A lot of code is only used within what you might call a "development process boundary". That is, the code and all of its clients are part of the same "codebase", and they are developed, built, and distributed to clients in lockstep. It is easy in such an environment to reason about all the uses of something: IDEs (or in simple cases, a simple text search) can quickly find all the references to a declaration within a program. Maintaining explicit access lists as the code evolves would be a significant burden with little upside. Furthermore, barriers like this within a codebase are largely enforced socially anyway: if you can't trust your coworker (or Future You) to not use a declaration they're not supposed to, even if it has a scary comment on it saying it should only be used by the X subsystem, you also can't trust them not to just add their code to the access list or just make the declaration public
.
Swift modules are always built in a single step, and so they must be deployed in a single step. Swift therefore assumes that it's reasonable to think of the code within a module as developed in lockstep --- even though, in theory, you could certainly have a single module built from hundreds of files submitted independently to the build by different developers with little cooperation. But Swift is an opinionated language, and we think that's not a good way to write a module, and so we decline to offer language tools that would be helpful to programmers in such a situation. Instead, we would encourage those programmers (assuming they can't just learn to cooperate) to divide their module into separate modules so that they can better encapsulate their submissions by taking advantage of internal
access control.
A single "codebase" developed in lockstep can certainly include multiple modules. We want to encourage people to make small, tightly-abstracted modules, and sometimes it's easiest to do that if they can still share a few private interfaces. (Even if you intend to whittle them down over time, the ability to start out with private interfaces can make it a lot easier to split up a large module.) The same reasoning above about specificity still applies across modules in these cases, since the modules are still developed in lockstep. So the Language Workgroup feels that this proposal embraces and supports the same principles of good development that motivate our approach to intra-module access control.
That's not to say that it's never appropriate to be more specific. When a declaration does need to be exposed to clients outside of that development process boundary, being specific is highly desirable. The @_spi
feature has been built around that idea; in fact, a major part of why we haven't made it an official feature yet is that we feel it isn't specific enough. The Language Workgroup is not closing off the possibility of making something like @_spi
official, but we do not think it will ever be a good way to address the use pattern of closely related modules within a single codebase. Nor do we think there is a viable path to generalize @_spi
to cover both use patterns; the resulting feature would be so complexly configurable that it would essentially become its own programming language, with attributes and rich interrelationships with modules.
Finally, while it is possible that a hierarchy of module groups would be useful to some programmers, we think that's unlikely. There is usually a clear boundary between the code that's developed in lockstep and the code that isn't. We are also conscious that access control is never perfectly precise, and as discussed above, we don't usually want it to be. It is better to have a simpler feature that pushes people towards good, common patterns than to have a more general feature that's slightly more precise in corner cases but is more awkward and painstaking to use in normal practice.
The package
keyword and the nature of packages
The choice of package
for the modifier gave rise to a lot of conceptual discussion about how to define a package and whether "package" is the right word at all. This can be thought of as two related questions. The first is whether "package" is a good general name for this concept of a group of modules. The second is whether the boundaries of SPM packages are the right place to be drawing the line, as opposed to allowing it to be drawn within or across packages. The Language Workgroup considered these questions very carefully; they were the primary focus of our deliberations.
"Package" is widely used in many different programming ecosystems as the name of the basic unit of code distribution. That is especially true for open source libraries, but many closed-source developers use the same mechanisms for managing their internal libraries. The boundaries of those units of code distribution almost always match our intuition above about what code is part of the same lockstep development process. There are exceptions, e.g. when a single package aggregates several loosely-coordinated projects, but they are very uncommon, and one could argue that they reflect poor package design. The Language Workgroup is loathe to pick another name that would be nearly synonymous with "package" in the vast majority of cases.
Not all Swift code is part of an SPM package, but not all Swift code needs to use this feature. The package
modifier described in this proposal is useful whenever there's a group of library modules that wish to share a private interface between them without exposing that interface downstream to their clients. The Language Workgroup is comfortable saying that anyone in this situation should think of themselves as writing a package even if they aren't necessarily using SPM. The basic language mechanism is not tied to SPM; any build system can pass down the right compiler flag to organize a few modules into a package as far as the language is concerned.
A lot of reviewers were concerned about the way that the proposal's application to SPM packages makes package
include every module in the manifest. There were several good arguments for why this was a problem. For example, a "black box" test should test what it would be like to use the package from outside the package, so it should not have special access to package
declarations. Other reviewers noted that splitting an SPM package up into multiple packages was a relatively heavyweight step, and so they sometimes found it useful to have multiple logical groupings within a single manifest; they would like to be able to make those groups different "packages" for the purposes of the package
modifier.
The Language Workgroup agrees that excluding targets from package
is a critical missing capability and asked the authors to revise the proposal to include it. We also asked the authors to simultaneously consider the ability to divide packages into multiple groups. In response, the authors have added a group:
argument to SPM targets which allows them to be excluded from the default package
group. This argument could be extended to allow targets to be placed in named groups, but the authors have left that as a future direction because it's unclear whether this is the right way to express that structure to SPM.
Grammar of the package
keyword
Many reviewers were concerned about using package
as a modifier because it is neither an adjective nor immediately suggestive of access control. There were many suggested alternatives here, but the Language Workgroup did not find any of them satisfying. protected
would be an attractive option except that it is already in widespread use with a specific, very different meaning. A compound keyword like packageprivate
would be quite long, and the Workgroup felt that it didn't add much that users wouldn't already understand from package
. The Workgroup was not concerned that users would confuse package
for an introducer that declared a package.
Subclassing
Several reviewers expressed concern about the lack of support for classes that can be subclassed but only within the package. This is a common pattern in code that idiomatically relies on subclassing, such as when working with Apple's AppKit and UIKit frameworks. The Language Workgroup shares this concern, but we're comfortable with not addressing it in this first proposal; unfortunately, finding a good syntax for specifying the spectrum of possibilities here is difficult because of how the current open
vs. public
syntax conflates two dimensions of access control (use vs. subclassing/overriding). In the meantime, programmers in this situation will have to continue using one of their current (admittedly unappealing) options: either make the class open
or put its subclasses in the same module.
@usableFromPackageInline
The Language Workgroup requested that this attribute be removed from the proposal. The attribute allows an internal
declaration to be usable in package
@inlinable
code. Since clients with access to package
declarations are already being trusted with special access to the module's internals, the Workgroup felt that there was not a good enough reason to allow expressing this exact possibility instead of just asking users to make the declaration package
.
Package names
Initially, the proposal tightly constrainted the package name that was passed to the compiler. Because the package name is deliberately not exposed in the source language, there is no reason to do this, and the Language Workgroup has asked these restrictions to be lifted. The compiler will simply treat package names as Unicode strings and compare them for equality.
Second review
In keeping with these conclusions, SE-0386 has been accepted in large part. It has been returned for a second round of review, from today until April 10th. That review is limited to the areas of revision, all of which were discussed above but which can also be seen in this evolution PR.
Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email. When contacting the review manager directly, please keep the proposal link at the top of the message and put "SE-0386" in the subject line.
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
- What is your evaluation of the proposal?
- Is the problem being addressed significant enough to warrant a change to Swift?
- Does this proposal fit well with the feel and direction of Swift?
- If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
- How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
More information about the Swift evolution process is available at:
https://github.com/apple/swift-evolution/blob/main/process.md
Thank you,
John McCall
Review Manager