RFC: What is the reason we lost scoped access?

A while ago I noticed private extension members became accessible from other extensions. As I discovered recently, the change happened before I joined the community, so I wasn't aware this went through the evolution process. I am raising this topic because I failed to see the compelling reason that lead to the
proposal's acceptance, or, rather, to the removal of scoped access as a byproduct. The proposal is named «Improve interaction between private declarations and extensions». Note there is no question of scoped access removal. Here is the problem stated therein:

In Swift 3, a declaration marked private may be accessed by anything nested in the scope of the private declaration. For example, a private property or method defined on a struct may be accessed by other methods defined within that struct.

This model was introduced by SE-0025 and with nearly a year of experience using this model, it has worked well in almost all cases.
The primary case it falls down is when the implementation of a type is split into a base definition and a set of extensions. Because of the SE-0025 model, extensions to the type are not allowed to access private members defined on that type.
...
Swift encourages developers to use extensions as a logical grouping mechanism ...

Within a file, allowing extensions to access private members of the primary declaration actually makes sense: the primary declaration should not hide anything from an extension.

However, if Swift encourages logical grouping and it was in fact (prior to implementing the proposal) a very popular technique to isolate functionality via extensions, why does the proposal touch the extension-extension case as well? The proposal mentions that SE-159 was rejected because «scoped access is something that many developers use and value», but itself ends up not only solving the problem it states, but also needlessly removing scoped access as a feature completely. Neither allowing extensions to access private members on a type nor preventing prevalent fileprivate usage requires removing scoped access between extensions or between a type declaration and an extension. What is the rationale for «improving interaction between private declarations and extensions» by removing an only partially related feature?

The prevalence of fileprivate in practice has caused mixed reactions from Swift developers, culminating in proposal SE-0159 which suggested reverting the access control model to Swift 2’s design. That proposal was rejected for two reasons: scoped access is something that many developers use and value, and because it was seen as too large of a change given Swift 4’s source compatibility goals.


Today, you can no longer declare a private helper method in an extension and expect it to be inaccessible from outside the extension without moving it to a separate file, which is rather frustrating.

IIRC the current private was changed from being a scope-private access modifier to a file-type-private access modifier with the vision of a softer default access modifier. That means the usage of the fileprivate should fade, but not be eliminated. The underrated issue of a scoped private was that it forced developers who tend to offload blocks of code into extensions to make more use of fileprivate than they wished to.


The following part of my answer will >>summarize<< my ideas from the linked thread, but is not meant to de-rail the current topic nor is it proposing anything in this thread.

Click to unfold.

I discussed my concerns about the current behavior because it's highly restricted to one camp of access preferences. Furthermore it creates some weird inconsistencies in names.

In this older thread I discussed on how this can be improved by eventually deprecating fileprivate by introducing limits to some access modifier.

Current Wish Comment
open - visible and can subclassed in outer lib (but in my opinion it should be generalised and also mean "can conform to a protocol from inner lib")
public - visible but cannot be subclassed in outer lib, can conform to a protocol from inner lib (I would wish that it would mean "cannot conform to a protocol from inner lib, but allows interface like usage of it as existential")
internal - -
fileprivate (file)internal true access of this modifier is on the same level as internal but restricted to a file
private (file)private this is a true file private access modifier because it's restricted to the type and the file
- private new soft default which is only bound to the type but not to a type

This would leave us with 4 access modifiers: open, public, internal and private. Access modifier below the public level would have a special optional limit (file) to bound them to a file. It allows to declare type private extensions in different files, which a lot of people would like to achieve without the need to expose the API as internal to other types. Furthermore it allows better testability with the potential future inline unit tests.


One could potentially extend the limits to allow scoped access but that's a different story (e.g. (scope)private).

FWIW, I am not proposing anything, I simply want to understand the compelling reasons we ended up losing scoped access on the grounds of «prevalent usage of fileprivate» (supposed to be a problem?) and «extensions cannot access private members of the primary declaration». None of those require removing scoped access between extensions or between a primary declaration and an extension (not the other way around).

The proposal states a problem, but takes away a whole feature apart from solving the problem.

Maybe @hartbit or @Douglas_Gregor as the review manager can explain how this happened?

What is the real world use case for private extension members ?

I assume that could be rephrased to «What is the real world use case for scoped access?». You can refer to SE-025, which introduced it. For example, suppose you have an extension that implements a conformance and you decide to write a helper function intended for that piece of functionality. Often you don't want the helper function to be accessible from outside the extension, either because it shouldn't be called elsewhere or is needless to the rest of the type context. Scoped access allows for better code structuring by expressing this formally.

2 Likes

I updated my response to make my intention more clear, I wasn't trying to suggest anything that you should propose nor did I propose anything in this thread. I simply tried to summarize my ideas from the giant thread that I linked to you in case you did not followed it back then or did not want to dive into it. ;)

Well more access control always would allow that in general, but I guess this topic historically always pursued simplicity over flexibility which in fact would imply complexity for both the compiler and the developer to learn.

My recollection is that scoped access was not deemed important enough to maintain as a separate access level, and wasn’t the right semantics for the obvious keyword (private) for less-than-internal visibility. We knew it could be added back with a different keyword if it became important, but the known use cases were not strong enough or common enough to justify making the model more complex.

The meaning of private we have now matches a particularly common use case—less-than-internal visibility with the ability to break up a type’s definition into meaningful extensions—that makes “private” generally the right default for less-than-internal visibility.

Doug

2 Likes

That’s how I would summarize the decision, yeah.