Function in private extension must be declared private because it uses a private type?

Is this a compiler bug? The function fails to compile with the error "Method must be declared private because its parameter uses a private type". But isn't a function declared in a private extension implicitly private? The error goes away if I declare the Foo enum outside the class.

class XYZ {
    private enum Foo {
        case baz
    }
}

private extension XYZ {
    func foo(foo: Foo) {}
}

private extension actually applies fileprivate to its members, unfortunately.

2 Likes

This originally made some sense under SE–025 when private was lexically scoped, since otherwise anything in a private extension would be invisible and useless.

But since SE–169 started treating same-file extensions as sharing a single scope, it would make a lot more sense to have private extension do what it says on the tin.

I suspect there’s no appetite for fixing this until and unless Swift gets a major-version bump with breaking changes though.

3 Likes

It does do what it says: members are made private to the scope where the extension is declared. That just happens to be less-private than the nested private--or rather, to be precise, it is by design that it is less-private in such a way. This was always the complicating factor about the new private. We explicitly debated this back in the day (and I specifically raised this issue during review, if I recall), and it is what we have.

Reasonable, though, to make the error more explanatory.

2 Likes

I feel like the error message still could be improved here and also some fix suggestions could be provided. Such as making the type have the required scope (fileprivate in case they are in the same file).

I think the appropriate place to file this bug is bugs.swift.org

There's definitely a documentation bug here. I filed something similar as SR-13196, and this error message is similar in nature. It really comes down to this line from TSPL:

If you define a type’s access level as private or file private, the default access level of its members will also be private or file private.

While you can do some mental gymnastics to make that true (the second "private" really means "private at the same scope level of the type itself"), this portion and the related section for extensions would, IMO, be more clear if they simply said something to the effect of "by default, members of a type/extension are as visible as the type/extension itself (with the exception of public type declarations, for which the default access level is internal)."

I remember this being discussed in the SE–0025 time period when people realized that private extension was essentially useless, so the behavior was changed to what it currently is.

I do not recall the behavior of private extension being discussed during the SE–0169 review, though I’d be happy to be proven wrong if you have a link.

• • •

Regardless of the history, the current behaviors of private extension and fileprivate extension are identical, and there is no way to write “an extension where all declarations are implicitly private”.

The latter would be useful nowadays, thanks to the SE–0169 improvements, and its natural spelling is clearly private extension, so if we ever get the chance to make this sort of breaking change I would be strongly in favor.

2 Likes

As someone who was against SE-0025 and SE-0169 I agree with @Nevin's conclusions:

  • Pre-SE-0025, private meant fileprivate, and so private extension meant fileprivate extension.

  • With SE-0025 (but not SE-0169), @xwu's explanation is correct: private extension is effectively still fileprivate because (a) applying private to all members would have been useless, and (b) it was a goal (though not one I agreed with) to make fileprivate rare if people wanted it to be. Therefore, "private means enclosing scope and that includes extensions, which are always at the top level of a file".

  • With SE-0169, private for all methods in an extension became meaningful again…but it would be a source-breaking change to have those methods not exposed to other types in the file. Perhaps that change should have been made in Swift 5 as a breaking change, and I'd be in favor of making that change the next time we break source compatibility, with a corresponding deprecation of private extension in favor of fileprivate extension in existing language modes when we do.

6 Likes

I'd be in favor of making that change the next time we break source compatibility, with a corresponding deprecation of private extension in favor of fileprivate extension in existing language modes when we do.

I’m not sure when the goal became a “was” instead of an “is”: it’s my understanding that it remains still an articulated goal of record that no one should need to use fileprivate if they don’t want to. After all, that was a big part of the justification for accepting that undeniably unfortunate spelling, and that spelling certainly hasn’t become any better with age.

It’s my own goal at some point to circle back to this and identify the few remaining circumstances where there is no ergonomic alternative to fileprivate and, after finding solutions for those, investigating the elimination of fileprivate altogether for good. Certainly I would not want to see any backsliding in this given the articulated goal.

Ah, yeah, I suppose I didn't mean to imply that it was not still a goal. I only meant "during the discussion it was brought up as a goal".

Personally I still think that SE-0025 and SE-0169 were mistakes, putting too much emphasis on types as access control boundaries. You're never going to be able to eliminate my use of fileprivate simply because I like to put helper types in the same file. But if I'm the only one using that idiom, so be it. (I consider myself largely responsible for the original access control levels from my time at Apple working on Swift 1.)

3 Likes

I loved (love) the original access control design, but since rolling it back has been conclusively rejected, we’re here where we are. That said, I don’t think your use of helper types in the same file hinges on the use of fileprivate:

If you label the top-level type as private (= fileprivate) and use the default visibility for internal members, then you get the same result.

One missing piece that would improve ergonomics is the ability to define stored properties in a same-file extension, which has been talked about elsewhere. This would allow arbitrarily deeply nested types to have their members visible throughout the file.

2 Likes

Yes, but if the top-level type is not private, you can't do that. If a stored property has a public getter but a fileprivate setter, you can't do that. And if the intended answer is "separate the public and fileprivate parts of the API into separate extensions", well, that's worse for me than keeping the word fileprivate.

It’s not clear to me that it’s terrible advice to have separation of a type’s public API from private implementation details. Many types in Foundation, for instance, have naturally gravitated to that style anyway.

The point about stored properties in same-file extensions applies to this situation, particularly when you don’t want to separate the public and private parts. It would allow you to declare a type and place its entire implementation, public and otherwise, in a private extension. (The intended design where extensions enforce maximum visibility was never entirely implemented, so that’s no longer a part of the design.)

public(get) has been pitched for other reasons here, but one reason I was delighted to see it (though I’ve never said it out loud) is that it addresses this issue.

1 Like