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
}
This current proposal would be served by:
private(to: .package) protocol AllConformancesCanBeViewed { }
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)
}
}