Confused by the behaviour of members in private extensions

I have a (dubious) minimal example Swift package consisting of two source files. One has the following content:

private extension Int {
    var isEven: Bool { get { self % 2 == 0 } }
    
    func isOdd() -> Bool { return self % 2 != 0 }
}

The other has the following content:

class MyClass {
    init() {}

    func a(_ i: Int) {
        let _ = i.isEven
        let _ = i.isOdd()
    }
}

Building this package with Swift 5.4 (swiftlang-1205.0.26.4 clang-1205.0.19.54) results in the error 'isEven' is inaccessible due to 'fileprivate' protection level (and another for isOdd()). Why do the errors say fileprivate instead of private?

I found that I can make the errors say private instead by making the actual members private as well, but don't members in extensions of public concrete types default to internal, not fileprivate? And even if they do default to fileprivate, the extension itself is still private, right?

Which brings me to my last questions: why does the package compile if I make isEven and isOdd() public? Are the members not restricted by the access level of the extension in which they're declared?

Is there something obvious that I'm misunderstanding about how access levels work? I'd appreciate keeping responses to discussions of why this behaviour happens, rather than whether or not writing code in any of these ways is good practice. Thanks in advance!

1 Like

private at top scope is equivalent to fileprivate on a nested declaration (because private there means “private to the containing type). It’s a bit odd.

Extensions don’t actually have an access level. Instead, the access modifier on extension is the default for declarations in that extension, overriding the default default of internal. It is, again, a bit odd.

3 Likes

Ah yes, I realise now that I vaguely had the sense that this was the case, but couldn't quite articulate it to myself earlier (after all, what exactly would it mean for an extension to be private - private to what?). I just found it mentioned at the end of this section in The Swift Programming Language. The warning you get when you do this also mentions that fact: 'public' modifier conflicts with extension's default access of 'private'. I gather the general idea is you should only make declarations more restricted than the default for the extension, but you technically can do the opposite if you want?

Is this documented somewhere that you can point me to? I don't think I quite understand what this means.

The same page I linked above states:

Extensions that are in the same file as the class, structure, or enumeration that they extend behave as if the code in the extension had been written as part of the original type’s declaration. As a result, you can:

  • [...]
  • Declare a private member in one extension, and access that member from another extension in the same file.
  • Declare a private member in an extension, and access that member from the original declaration in the same file.

The text is careful to state that this only applies when you declare the extension in the same file as the original type, and doesn't appear to address the alternative case (which would apply to any type defined outside of your own source code, as in my example).

This has been discussed previously, and Jordan Rose gives a good overview of how we got where we are in this comment.

The upshot is, currently private extension and fileprivate extension are synonymous, even though SE–0169 “should” have given them distinct meanings.

I don’t think this was actually missed during review of SE-0169; if I recall, it was brought up during review, and the core team declined to do so.

The original design as approved was that it would be a compiler error to make something inside an extension more visible than a specified default on the extension itself, but the implementation was unable to do this for a long time. This is in distinction to the design approved for more visible access levels on members inside types, which are explicitly allowed.

With the passage of time, enforcing the rule for extensions could only be a warning because an error would cause existing code to stop compiling.

Could you please provide a link to that?

Searching the SE–0169 review thread for “private extension” yields zero results.

My recollection is that it was not brought up until after the review. And in particular, that it was you yourself who first mentioned the issue in the SE–0169 acceptance thread.

The first response to that, 2 posts later, by the review manager themself, was a resounding Yes:

It does imply a change to "private extension" semantics: the members of such an extension would be "private", so they'd be visible to other extensions (and possibly the definition of) the extended type that occur in the same file.

The next post, by another member of the core team, claimed that doing so would also require declaring top-level functions and typealiases with fileprivate, which seems like a complete nonsequitur to me.

private function at the top level would obviously still be synonymous with fileprivate function. It is only private extension that would gain its obvious meaning of “the members of this extension are implicitly private”.

Ha, I had forgotten about that: thank you for reminding present-me about what past-me was up to. I’m pleased the core team agrees with this point, too bad it hasn’t been enacted...

1 Like

Unless we ban top level contextual private and force fileprivate (unlikely) we are stuck in the canal ( or the ship has sailed?).

//top level

private struct Ship {
  var isMoving = false
}

private extension Ship {
  mutating func move() { isMoving = true }
} 

Here isMoving defaults to internal but since the type itself is contextually private at top file level then isMoving gets lowered to top level private (fileprivate)

The move() function similarly is internal but raise/lowered to match the extension access control modifier.

I strenuously disagree.

You are saying “If we do X then we must also do Y”, but in fact there is no such rule of inference.

It is perfectly sensible to say, “private extension means every declaration within the extension is implicitly private,” without making any changes to anything else at all.

Notably, this can be done as a purely textual transformation of source code: Anywhere that private extension occurs, find all declarations within that extension which do not have their own explicit access modifiers, and paste the word private in front of them.

This is the equivalen of tabs vs spaces of Swift. :slight_smile:

What? That sounds completely unrelated.

Tabs vs spaces is a style preference.

“It is possible to change the meaning of private extension without changing the meaning of other top-level private declarations” is an objective fact.

Disagreeing with that is not a differing opinion, it is simply being incorrect about the facts.

To me, it appears that this is not explained in the documentation (though now that I have the information, I can see how the portion from TSPL that I quoted earlier might imply it). Is this the case? In particular, is there something that I should have read, that would have told me why the error I received referred to fileprivate when I had, to my knowledge at the time, not given anything that access level? This was a fairly inscrutable error message for me, and I'd assume I'm more engaged in Swift's development history than the typical user.

I don't think we should expect a user, especially one new to access levels, to (be able to) become familiar with the current situation wrt SE-0169 (and potentially SE-0025, SE-0159, and others).

Basically, I'm unsure why TSPL's section "Private Members in Extensions" only mentions the case where the extension is in the same file as the class/struct/enum it extends, doesn't mention the opposite case, doesn't mention fileprivate, and does not refer to protocol extensions at all (but then proceeds to use a protocol extension as the only code example).

1 Like