Can the `package` access level be implicit in certain contexts?

Code size would also suffer with current tooling, since unused package declarations would not be subject to the compiler’s dead code elimination passes.

5 Likes

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.

4 Likes

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.

1 Like

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.

4 Likes

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").

1 Like

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.

4 Likes

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?

1 Like

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.