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

My question is pretty simple. Is there a reason why declarations are not implicitly package when they are members of a package type?

(By the way the unfortunate grammar when speaking about this keyword has been seeming pretty inconvenient to me in commit messages, and now also here in this post - not much we can do about it now, but suggestions about the best way to phrase things are welcome).

Example of my suggestion:

package struct SettingsPageViewModel {
    var isAutoSaveOn: Bool // This has `package` access level
}

I'm currently having to litter package all over my code, and it could be reduced by something like 85% if this inference that I'm talking about were implemented. Is there an important reason why it is not already like this? To me it would seem to be the more sensible default. If I've already marked the type as package, thereby expressing that is relevant to other modules within my package, then to me it seems more common that members of that type will also need to be exposed at that level. I can always mark something explicitly as internal if I want. And I believe that the reasons that we don't treat public in this same way are not relevant with package.

Thoughts?

2 Likes

i disagree with this motivation, today there is an easy-to-understand rule that if there is no ACL specified, then the effective ACL is min(internal, enclosingACL). when we mark types as public, we do not expect that all the nested declarations also become public. so it would be confusing if package were heritable but public is not.

i agree with you that the choice of keyword spelling package is unfortunate.

7 Likes

If we made this change I'm proposing, then the rules to learn would be:

  1. "The visibility of an unmarked type is internal".

  2. "The visibility of an unmarked extension is inherited from the type being extended".

  3. "The visibility of an unmarked member is inherited from its enclosing scope."
    (scope = extension or type declaration)
    (member = property, function, subscript or type)

  4. "If you are an "advanced" enough developer to be maintaining libraries then learn the relatively simple caveats to rules 1 through 3 relating to public."

To me this seems very intuitive, and therefore simpler to learn than the current rule.

The exceptions for public are a deliberate sacrifice of convenience for the sake of avoiding accidentally fragile API surfaces. For package however I believe that there is basically none of the same upside to forcing developers to be explicit about every single declaration, but there is of course the same syntactical inconvenience as the downside.

So, I still currently think it would be a big win to make this change. What do you think? Do you think I'm missing something in my reasoning?

2 Likes

Recall that (for various reasons) the access modifier in front of extension means something different than an access modifier anywhere else in that it is a shorthand for applying the effective visibility denoted by that access modifier to all the members contained in the extension.

So, for methods and computed properties, you can put them in a package extension (and likewise public extension) and they will be inferred to have that access level.

1 Like

Yes, to be clearer, the inconveniences that IIUC are basically useless and that I’m proposing to eliminate are:

  1. Having to write package on extensions of package types.

  2. Having to write package on stored properties and on any declarations that for whatever reason I would prefer to put in the original type declaration.

  3. (and this one is an extra step that I haven’t explicitly mentioned until now) having to hand-roll memberwise initializers if I want them to be package-visible for package structs.

Right, as originally proposed, package has the behavior of a "narrower" public, and your argument is that it ought to be that of a "broader" internal.

This rhymes with the recent amendment to SE-0364 about package and @retroactive.

If it is felt to be a convincing argument, then I think we ought to revisit the feature wholesale on that basis instead of patching the rules one-by-one.

3 Likes

Cool, well obviously +1 from me.

Separately, regarding this:

I thought you helped me discover that this is not the case (despite me believing to be so for the past decade). Do I misunderstand what you’re saying here?

What was the thinking behind that? It's not apparent to me how package is any in real sense like public.

I see value in internal but it's along the same lines as fileprivate and private - it's more to keep yourself (and/or your co-workers) honest, than it is about API. As such, I tend to agree with @jeremyabannister that it's not apparent what purpose the current inference - or lack thereof - is actually serving. That's in contrast to public, where I think Swift's approach has proven to be wise (even though it was controversial at the outset).

But perhaps others structure their packages very differently? I do sometimes put more than one module in a package, but it's purely about linker aspects, not really access control. e.g. to fractionate such that dependants don't have to pull in more than they really want.


P.S. Personally I think package is a perfectly good name - it's internal which is poor, because what does that even mean, on face value? Internal to what? It would have been much clearer if it were module (or some passable synonym, e.g. library).

1 Like

A long time ago I suggested a possible syntax to address this…

The effective visibility of private written at file scope (which all extensions are) is the same as fileprivate. For this reason, all members of a private extension are visible throughout the file. The effective visibility of private written inside an extension is scoped to that extension (and others in the same file).

Thus, private extension T { var foo ... } is not the same as extension T { private var foo ... }. This should not be altogether surprising: the visibility of foo in private struct T { var foo ... } is also not the same as that in struct T { private var foo ... }.

You can do so with package as well.

I disagree with the motivation. If package was inferred on the type's members then the access modifier would automatically expose possibly unwanted implementation details to clients inside other modules of the same package. Additionally this introduces more inconsistent mental load on the language user which then has to remember that the access and visibility rules would apply differently to package and it would defy the already established internal default.

TLDR; Introducing more access and visibility should always be opt-in, not opt-out.

5 Likes

While convenient for the author, it would remove locality of reasoning, which would make it harder for the reader to understand the code. We should optimize for the reader, not the writer.

What you're proposing would let an author do this:

// File1.swift
package struct Foo {}

// File2.swift
extension Foo {
  func bar() {}
  func baz() {}
}

What visibility do bar and baz have? With today's rules, you only have to look at File2.swift; they're internal. With your change, you wouldn't be able to tell without also tracking down the original type declaration in a different file.

11 Likes

FWIW, the original motivation behind internal-as-default is “you don’t have to think about access control if you just have one module, usually an app module”. Sure, this was partly motivated by moving from ObjC (where access control is “I put the declarations in a file other people can read, or not”), but I think it’s still a reasonable principle today.

Unfortunately, it doesn’t offer so much guidance here. Some people would have preferred fileprivate, with the principle being “if you just have one file”. And now it’s reasonable to prefer package as well, with the principle being “if you control all the source”. :person_shrugging: Personally, I think how it is is still a reasonable compromise.

5 Likes

Well, effectively, it would make package the new internal, except in the case of naked struct Foo. My reply above was that if @jeremyabannister's argument applies for extensions, then it should be applicable everywhere, such that for Swift N, naked struct Foo should be equivalent to package struct Foo. This would resolve your objection.

1 Like

Indeed, all three are defensible.

But that reasoning was also originally made in a relative vacuum - e.g. it long predated Swift Package Manager, which effectively standardised on Git repos as the method of compartmentalisation. Which means package-level, essentially.

Given that the key distinction really is whether something is exported, that makes package the important scope. Having to repeat public (or open) every time is the right choice, because the potential downsides of a mistakenly publicised API is huge. In contrast, having to repeat package / internal / fileprivate / private doesn't give you much, at the expense of spurious compilation errors and increased boilerplate.

It's also possible to have linters which mandate explicit (but technically unnecessary) package redeclarations for all members, if someone really wants (might make plenty of sense in some large organisations). Whereas right now the compiler is forcing that on people without a choice.

1 Like

Agreed of course, the reader's wellbeing is paramount. Are you sure though that the reader is better off in the current situation?

One thing that a reader will often want is to find all places where the member is referenced. I'll explain myself in code to keep it more concise and precise:

func howCanReaderFindAllReferences(
    to member: MemberDeclaration
) -> Recourse {
    
    switch member.explicitAccessModifier {
    case .public:
        .impossible

    case .private, .fileprivate:
        .scrollOrSearchCurrentFile

    case nil, .internal, .package:
        .useXcodeGlobalSearch
    }
}

Since internal, package and the absence of an explicit access modifier all imply the same recourse for the reader, I contend that on this particular front (that of finding all references to the member) there is no harm to the reader if the absence of an explicit modifier will sometimes mean the declaration has package visibility.

Furthermore, in cases where the reader already knows that the type has package visibility and the majority of the members also have package visibility, then the package modifiers are visual noise that actively harm the reader. Of course, if the majority of the members have internal visibility then the current situation is better and my proposed change would be worse, because then there would be a ton of internal modifiers creating noise, so this sub-point comes down to the question of whether the majority of the members of a package type will be generally be internal or package.

Lastly, regarding this:

I just want to point out that one will always be exactly one jump-to-definition away from the answer due to the fact the ambiguous extension will always spell out the type name in question. Jump-to-definition is of course significantly more effort than having the answer already present in the code in front of you, but I think this is nonetheless a relevant point.

With all of this in mind, can you describe some ways that I haven't mentioned here in which the reader would be harmed by the proposed change?

as a high-level goal, i think we should strive within reason to keep the rules of the language as simple and easy to teach as possible. we don’t have any sort of ACL “inheritance” outside of public extension today, so special-casing package to inherit everywhere would be another behavior to teach as part of the “Rules of Swift Access Control”. i don’t think the benefit of enabling people to say package less often outweighs the added complexity.

2 Likes

That’s not quite true: enum cases and protocol requirements both match their containing type’s access, and there are moderately compelling reasons to allow changing that without changing the default behavior. But even without that, the topic under discussion is about changing the current default of internal to a new default of package, which is only slightly more complexity to explain for a new learner. It’s not a whole new rule.

2 Likes

As annoying sometimes to write all access modifiers explicitly, in the target with intermixing levels, I find (as a reader mostly) having explicit modifiers in front of most of the types (including internal and avoid access modifiers on extensions) as huge benefit, so that it is obvious by just looking at the code which visibility level it has, without any need to remember special cases. Explicit is better than implicit.

In that way, adding new implicit behaviour to visibility modifiers seems to be undesirable IMO, keeping that more explicit. Swift has its ACL increased in complexity over the years just in terms of how many levels of control we have, and while all of them make sense and are useful, I’d better took away some of the features (as extension case) than add more.

Arguments for making package the new default are indeed compelling. You control all the modules of a package so you’re not exposing public API and, hence, incur no substantial limitations. However, I’m really concerned about compile times. By implicitly exporting everything to other modules in a package, we risk substantially degrading type-lookup and type-checking performance. This penalty is not only incurred by the package author, but also by downstream packages relying on this package. Not to mention, that many apps today are already written in one module where the default internal access level is sufficient.

In other words, if a developer chooses to create a package, they should incur some extra boilerplate. These explicit package modifiers would in turn avoid significant slowdowns in compilation for themselves and downstream packages (which are affected exponentially).

4 Likes