Speaking just for myself, I think it makes sense to have access levels for both modules (smallest unit of library organization) and packages (smallest unit of code distribution). Both boundaries are important to be able to enforce: modules because we want to encourage small, encapsulated libraries with well-designed interfaces, and packages because it's almost certainly a boundary between different development organizations.
I don't think analogies to intra-module access levels are really on point. Intra-module access levels are designed to promote encapsulation of small subsystems that can't quite be extracted into separate modules. At most, they might reflect organizational differences within a large development group. I do actually think there's space for more levels within modules — I keep toying with the idea of a directoryprivate that would help teams to promote/enforce those subsystem boundaries without requiring code to be broken into separate modules (not always possible) or combined into single large files (distasteful and likely a build-time hit) — but on some level, this is all arbitrary because the code will ultimately be built in a unit. In contrast, modules and packages have clear provider/client relationships with a direct need for access enforcement in order to maintain a well-defined API and avoid unwanted dependencies.
Food for thought: could there be a way/would it be a positive change to have some sort of design for user-definable access modifiers? Perhaps it’s an off-topic idea, since I can’t currently imagine how this would solve the need at hand (i.e., we would still need the package modifier or whatever we end up calling it), but in case it inspires any interesting or important ideas in others I'll at least describe it a little bit.
An example is that it could be useful to make an extension on one type that is private to another type, like this:
private(to: Dashboard) extension Double {
static var minimumSectionHeight: Self { 40 }
static var standardCornerRadius: Self { 12 }
}
which can then be used only in the scope of Dashboard:
That would be interesting, I think the excitement around SwiftPM and how easy it seems to be kind of promoted distributed monoliths where you are splitting an app in multiple packages hosted in their separate repos but effectively are not separate at all.
This is an area where to be frank all a lot of devs need is fulfilled by the package visibility you have with Java projects where the only thing you need to do is setup the folder structure to create a “module” where you can be flexible about visibility inside the module and strict between modules without much bureaucracy really.
i’ve been thinking about this for the past week or so and i’ve gradually come around to the conclusion that a new access modifier like package/external is not the right solution to this problem.
because i agree with @Joannis_Orlandos that i too often find myself reaching for something like package and i end up just making it public even though it is not really “for public consumption”, it is only public so that i don’t end up cramming everything into one module. and that just leads to a lot of underscored APIs and doccomments that scream “don’t call this manually!” which i dislike.
but the more i think about why i reach for package in the first place the more i realize it is something of an XY problem because “access control” really encompasses two things that get conflated with one another:
enabling local reasoning (“i can change this thing while being aware of all the other things that could potentially be affected by this thing”)
encouraging correct usage (“i can’t plug the USB(-A) stick backwards because it won’t fit”)
because what package does is provide #1 but a lot of times what i really wanted to accomplish is #2. and i think the local reasoning that package would enable is not actually very useful in practice, because the more code that local refers to the less helpful locality is in the first place.
for example internal can facilitate some amount of local reasoning, but it is nowhere near as powerful as fileprivate. and fileprivate itself is not as powerful as private. and that’s why i think a level above internal, like package/external, would not be helpful because when “all the other things that could potentially be affected by this thing” means “everything in this package”, and the package itself can be quite large, that really doesn’t mean much.
and when i imagine what really would help accomplish #2, i think that solution looks more like @_spi, which would align a bit better with some of the other ideas pitched in this thread, such as:
While in the end I'm not in total agreement with the concept as stated, I appreciate the formulation of it and it led me to some new ideas.
I would change "encouraging correct usage" to "enforcing correct usage", and I would further add that I think the benefit of enforcing correct usage always boils down to local reasoning (e.g., an initializer that must be called just-so is made private, which enforces correct usage of the type by making the user use the robust public initializers, and the purpose of this design is that the author of the type can locally reason that the type's properties will never be configured improperly.)
Therefore, I'm arguing (at least until I'm convinced otherwise) that maybe there's only one reason for access control modifiers, which could be described as "local reasoning".
Since that's what I'm arguing, the next step would be to refute this:
to which I would point out that "a Swift package that I am currently authoring" is a waaay more "local" scope than public (and also of course it's much less "local" than "the code in this single file"), and consequently marking something packageprivate gives me tremendous local reasoning capabilities as compared to public. For example, if I have a packageprivate protocol I have the ability to physically view all of the conformances to that protocol that exist in the entire world, just by browsing the package's source files that I have locally on my computer, which is a noticeably easier task than the impossible task of viewing all conformances if the protocol is public and on GitHub (and, of course, is also a correspondingly harder task than viewing the conformances if the protocol is fileprivate.)
New Idea - General Solution
I'll finish with an idea that has just formed in my mind, which I think would qualify as a more general solution to the problem:
Continuing off the idea I offered above, imagine that we introduce a new access modifier:
private(to: RelativeScope)
where conceptually we can imagine the RelativeScope type like this:
enum RelativeScope {
case package
case module
case folder
case file
case type
}
These current modifiers would desugar in these ways:
internal is private(to: .module) fileprivate is private(to: .file) private is private(to: .type)
It would be pretty awesome to get folder to come along for the ride.
Later on we can add new overloads for this access modifier if we determine new scopes that would be useful. For example, earlier in this thread in a pseudo-code example I made use of this overload:
private(to: Any.Type)
As a final new idea, I'll mention the possibility of adding the case named extension to the imaginary RelativeScope enum, which would be useful in this way:
/* we're inside of the somewhat long DashboardView.swift */
/* ... code, code, code ... */
/* ... not visible because it's scrolled off the screen ... */
private extension DashboardView {
var balanceReadout: some View {
HStack {
balanceNumbers
currencyToggle
}
}
/// I can see at a glance that this subview
/// is not used anywhere other than in `balanceReadout`
private(to: .extension) var balanceNumbers: some View {
Text(viewModel.balance(in: selectedCurrency).description)
.bold()
}
/// The same luxury of local reasoning applies to this one too
private(to: .extension) var currencyToggle: some View {
Toggle("Currency", isOn: $selectedCurrency.dollarsOrEuros)
}
}
"The only clients of this are in this package" vs "the clients of this are external" seems like a very meaningful difference to me in terms of reasoning about the effects any changes might have. If a decl has package level visibility, it means if I change the name, the type, the signature, etc. that I can be confident that all consequences of that change can be addressed by updating code that I have access to and control. For a public declaration, on the other hand, those details may be a contract I need to maintain indefinitely if I vend a package that provides source stability. When I was an SDK framework developer, being able to make this distinction would have been incredibly useful.
you are referring to “public as-in for public consumption” but i was referring to “public as-in uses the public keyword”, which seems to be the main motivation for package.
I have mostly developed packages in what some might call a "live at head" environment. In such an environment, relying on the assertion that some of the public surface of my package is source unstable isn't really an option because it's unenforceable. If my package exposes a public decl and there's a chance that some other package in the ecosystem might be using it then I simply cannot change that decl without tracking down the uses and coordinating a staged change with the affected package owners. If I break the build for those other packages, it's my change that's going to get rolled back. And to make matters worse, sometimes just even identifying whether any other package uses an API can be challenging. The package access level solves this by allowing me to build inter-module APIs without needing to worry about the implications of exposing internals.
Even when maintaining source stability for other packages is less of a concern, though, I think that this language feature is well justified. Breaking encapsulation by exposing the internals at all, uglifying the names with underbars, and needing boilerplate documentation to remind users that "this interface isn't actually for you" are significant compromises. This feature is pretty analogous to what Xcode refers to as project headers for C like languages and it was surprising to me as a long-time Objective-C framework developer that there was no analog to this useful encapsulation tool when building a module in Swift.
The package modifier does behave a lot like a special case of the @_spi attribute, so I think it's reasonable to wonder about the relationship between the features. One way to approximate the functionality of this modifier today would be to write the package decls as @_spi(Package) public func foo() { ... } and then in all of the modules in your package prepend @_spi(Package) to every import <Foo>. However, @_spi is really meant to be a tool for curating the external interface of a module for consumption by separate levels of external audiences. In the live at head environments I described earlier, @_spi decls have the same source stability requirements as standard public decls; it's just a bit easier to track down and coordinate with the users of @_spi.
To illustrate why I think package and @_spi serve different purposes I'll again make an analogy with Obj-C/C framework headers:
Swift modifier
Obj-C/C header visibility
Intended audience
public
Public
all modules
@_spi(...) public
Private
authorized external modules
package
Project
modules in the same project
I think that each of these distinct levels of visibility is important to have as a library owner and therefore I wouldn't want to see @_spi and package combined into a single feature. Could you establish a convention where @_spi(Package) declarations are understood to be only for use in the same package? Yes, but unless the tooling also understands the convention many of the potential benefits of the proposed feature are lost. If you want to be really sure no external parties are using your package interfaces, then the compiler should prevent use of @_spi(Package) decls outside the package. And something like "whole package optimization" is only possible if the compiler and other tools know which declarations are used outside of the package and which are not. Since the main benefits require that these declarations be treated differently than normal @_spi declarations, I think it's better to just have a distinct spelling that makes the difference clear.
I think there’s a stronger version of this argument: @_spi public declarations have the same binary stability requirements as normal public declarations. Since the whole package is built as a unit, package declarations can have unstable—and therefore more efficient—ABIs.