SE-0386: `package` access modifier

Let me take my review-manager hat off for a moment and talk this through as a language designer.

Swift's central goal for access control is to encourage what we see as good program design, where you can break your code down into well-thought-out layers of libraries that can independently evolve and grow as you continue to work on them. This goal can sometimes come into conflict with the goal of feeling "lightweight", which in many ways comes down to discouraging programmers from brooding endlessly over details that don't really matter. That could certainly happen with access control, if it were too fine-grained.

The balance that Swift strikes is centered around the idea of co-development. Different groups of people work on different parts of a program. Swift's access control encourages you to define boundaries between parts that can be thought of as little library units: types, files, modules. That's how we promote that first goal of layering and code re-use. But as long as you have the same people working on the same code, these boundaries within the code aren't that important. People working on the same code need to be talking to each other anyway, and they can agree on standards, and they can repair any little breaches just as easily as they broke them in the first place. It's when the boundaries in code reflect real differences between groups of programmers and how they contribute their software to the whole that we first run into hard barriers to evolving the code; those are the boundaries that are truly important to enforce in access control. Without that in play, there's no reason we shouldn't keep things lightweight.

The problem we're looking at here is that the language doesn't let a group of programmers simultaneously enforce those boundaries with other groups — the only boundaries that are truly important to enforce — while also splitting their own code into multiple modules, unless the modules don't need to share any private interfaces with each other. That's a significant problem that I think is worth addressing.

The design of the @_spi attribute really is focused on needing to poke a very specific hole in access control across one of these boundaries between groups. That's why it requires a group name on both declaration and import, and it's why individual files need to import the SPI separately: those are precautions meant to strongly discourage other uses of the SPI. That might not be how some other people use the term "SPI" (although I wasn't aware this was a common term outside of Apple? The other uses I can find on the internet are unrelated), but it is explicitly how that attribute is designed. The effects of that are not what we want for boundaries that are purely internal to a group, because that gets us back to programmers brooding on unimportant things; even just in this thread, it sounds like the people who want to use the attribute this way are talking about drawing all sorts of fine-grained distinctions.

I am very open to the idea that package is not the right term for this concept of a bunch of code written by the same programming group. (Put aside other questions about the keyword for a second, like whether it should have a -private suffix.) The very nice thing about package as a term is that it directly invokes a very important use case for this: related library modules in a single open-source package. But that comes with two disadvantages that I can see:

  • Other kinds of programming groups can have the same problem without necessarily thinking of themselves as writing a "package". The team writing the data layer for a medium-sized app might well have the exact same problem of wanting to split their code into multiple modules without giving access to their internal APIs to the UI layer, but they'd probably find it strange to see package throughout their codebase referring to their team's code.

  • It's not necessarily true that everything in a SwiftPM package ought to have access to package-private APIs. I'm not particularly motivated by the idea of a package that contains code from teams that don't closely collaborate — that seems like a perfectly reasonable thing for SwiftPM to be opinionated about, and if that discourages using package in such packages, so be it. But @taylorswift's example of a sample project that should only be using public API seems much more compelling. I believe SwiftPM is looking at features for making this kind of supplemental code explicit in a package, so that might give an avenue for excluding them from the scope of package, but you can already see the difficulty here: it's hard to talk about this difference, and it might be a little surprising that anything in the package wouldn't have access to package-privatte APIs.

I wonder if it might not be better to use a keyword that recognizes the organizational differences being assumed here, something like teamprivate. Using a (slightly) longer compound keyword also has the benefit of calling out the unusually-expanded scope, the same way that fileprivate calls special attention to the fact that a declaration needs to be used from other scopes within the same file.

17 Likes