Code size would also suffer with current tooling, since unused package
declarations would not be subject to the compiler’s dead code elimination passes.
Swift access control feels like it's never really had a chance to "settle". I think no matter what we do, some users are going to want to have subtle changes to it that make their specific workflows more convenient. Continually tweaking it isn't really a sustainable way for the language to evolve, especially shortly after a new feature like package
has landed, and it would be very detrimental for user education because old documentation becomes obsolete and it's not always clear to new users whether they're reading something that applies today or applied one, two, or five years ago.
While I can see the arguments above for why package
is a reasonable default, I don't think it's necessarily a more reasonable default than internal
today. The module is an important boundary in Swift today—it's the compilation unit of the language. A lot of important compiler behavior depends on it, like @tshortli mentions above. The package boundary is more arbitrary; it's a convenience for SwiftPM but all that really matters is that two modules pass the same -package-name
to the compiler. That feels like a potential access hole across multiple modules.
And what about the idea that I originally proposed, which is to leave the default visibility of top-level declarations as internal
but allow the package
visibility of a type to flow through to all of its unmarked members? This is perhaps already how private
/fileprivate
could be interpreted to work, and it would be very convenient for package maintainers. Any responses to my argument that the readability is potentially improved rather than degraded?
To be clear, though, that's not the argument made by the original poster (@jeremyabannister). He simply suggested inheriting package
for type members. package
still has to be explicitly used by the author, otherwise the default is still internal
. It's simply about eliminating some boilerplate (package
re-declarations on every member).
I think the only conceivable counter-argument is that it'll make it harder to look at a member declaration in isolation and know whether it's internal
or package
. But then you already can't tell if it's internal
or fileprivate
or private
, so I don't think that's a significant change.
The comparison to fileprivate
and private
doesn't really hold here because the type's access level is an upper bound. If someone is looking at a default-access member of a fileprivate
type (not counting extensions), the question "is this internal
or fileprivate
?" doesn't actually matter because they would need to have a reference to that type in some other file, which is already an impossibility. Letting package
propagate through members is a different thing entirely, since it makes defaulted access do something different for one specific access level and in the opposite direction: propagating a higher access to members.
Yes, but the converse is also true today - if a type is declared package
, why can't I use it (meaningfully, i.e. access its members) from elsewhere in the package?
So I think the point stands. If you just see var exactThingINeedRightNow
you either have to try it and face a compiler error, or scroll up to the type's declaration searching for an access modifier there, in order to know whether you can actually use it.
And in practice I can't recall that ever being a significant problem (not enough to require explicit access declarations on every declaration, at least). Which makes me think making package
"override" internal
locally as the default isn't actually going to hurt anything.
I'm also not convinced it adds conceptual burden - the idea is really trivial ("the default access level is internal, unless you explicitly make it package").
Which members? If you have a simple POD type, what you say starts out sounding reasonable, but then you have to answer what happens with more complicated types. A totally reasonable thing to do is to have a package
type that can be consumed elsewhere in the package but I only want to be able to initialize it or do certain operations in the module where it's defined. Then I need to use explicit internal
, something I don't have to do elsewhere. It's these death-by-a-thousand-little-inconsistencies that make code harder to reason about and why I think we should avoid adding more complexity to an already complex system.
Is it more complexity, though? It doesn't seem it, to me. It's just changing some defaults.
You already have to worry about nuances like fileprivate
and private
- those aren't "automagically" handled for you, even today.
Jeremy's just proposing allowing users to choose package-level modularity as the default instead of module-level.
By that notion, perhaps an alternative is to change this setting at the package level? e.g. in the Package.swift
. Perhaps that's lower perceived cognitive burden for folks, since you need only realise and remember that the whole package uses package
by default instead of internal
, without having to search for it on individual symbols?
package
is in a somewhat awkward situation. It was introduced with the motivation that the portion of the code in the package that is only expected to be accessible to different targets should not really be made public. In other words, it is both internal
and public
. I recommend avoiding package
as much as possible, unless you really need to make it public to multiple targets, in which case use package
instead of public
if necessary.
I like the trick @xwu said. Declare package
explicitly once before extension
to reduce repetition, and you can write code sorted by different extensions.
In the context of avoiding package
as much as possible, I think more red tape leads to fewer surprises. -1.