Some very good points. But I’ll concentrate on what I disagree on to see where we can go from there.
> On 23 Feb 2017, at 22:56, Nevin Brackett-Rozinsky via swift-evolution > <[email protected]> wrote: > > Introduction > > There has been a deluge of discussion about access levels lately, all > attempting to simplify the situation. Shortly after Swift 3 was released, > many people realized that the new access modifier story was far more complex > than the old one, and expressed the belief that the changes may have been a > mistake. > > In the months that followed, more and more people came to share the same > view, and stage 2 of Swift 4 has seen a profusion of proposals addressing the > issue. These proposals are generally small and focus on changing just one > aspect of access control. However, given the situation we are in now, it is > important to look at the problem in its entirety and arrive at a cohesive > solution. > > Background > > During the Swift 3 timeframe there were lengthy debates about access control. > The end results were to adopt SE-0025 > <https://github.com/apple/swift-evolution/blob/master/proposals/0025-scoped-access-level.md>, > which introduced the ‘fileprivate’ keyword and made ‘private’ scope-based, > and SE-0117 > <https://github.com/apple/swift-evolution/blob/master/proposals/0117-non-public-subclassable-by-default.md>, > which made ‘public’ classes closed by default and introduced the ‘open’ > keyword. At the time, there was broad agreement (and some dissent) that these > were the right changes to make. > > That belief, as well as the numerous arguments which led to it, were > well-informed and thoughtfully considered. However, due to the inevitable > linear nature of time, they were not based on first-hand experience with the > new changes. Upon the release of Swift 3, we all gained that first-hand > experience, and it quickly became apparent to many people that the new access > control system was needlessly complicated, and not at all the improvement it > had been heralded as. > > Rather than make it easy to encapsulate implementation details of related > types across multiple files, we had instead doubled down on requiring that > many things go in a single file or else reveal their secrets to the entire > module. Even worse, the new scope-based ‘private’ discouraged the preferred > style of using extensions to build up a type. To cap it off, we went from > needing to know two access modifier keywords (‘public’ and ‘private’) to > needing to know four of them (‘private’, ‘fileprivate’, ‘public’, and ‘open’) > without even providing a way to share details across a small number of > related files. > > Motivation – Overview > > Many different ideas for access control have been expressed on the Swift > Evolution mailing list. Some people want ‘protected’ or ‘friend’ or > ‘extensible’ or various other combinations of type-based visibility. Other > people (*cough* Slava) see no need for any levels finer than ‘internal’ at > all. The common points of agreement, though, are that ‘fileprivate’ is an > awkward spelling, and that the whole system is too complicated. > > It is important that we as the Swift community are able to recognize our > mistakes, and even more important that we fix them. We originally thought > that the Swift 3 access control changes would be beneficial. However, > experience with them in practice has shown that not to be the case. Instead, > the language became more complex, and that has real costs. It is time for a > simplification. > > The prevailing view from recent discussions is that there should be just one > access level more fine-grained than ‘internal’, and it should be spelled > ‘private’. Let us leave aside for the moment what its exact meaning should > be, and consider the other end of the scale. > > Motivation – Rethinking ‘public’ > > Prior to Swift 3 we had just one access level more broad than ‘internal’, and > for simplicity of the model it would be desirable to achieve that again. > However, SE-0117 raised the point that certain library designs require a > class to have subclasses within the defining module, but not outside. In > other words, client code should not be able to create subclasses, even though > they exist in the library. Let us be clear: this is a niche use-case, but it > is important. > > The most common situations are that a class either should not be subclassable > at all—in which case it is ‘final’—or that it should be subclassable anywhere > including client code. In order for a library to need a publicly closed > class, it must first of all be using classes rather than a protocol with > conforming structs, it must have a hierarchy with a parent class that is > exposed outside the module, it must have subclasses of that parent class > within the module, and it must also require that no external subclasses can > exist. Putting all those criteria together, we see that closed classes are a > rare thing to use. Nonetheless, they are valuable and can enable certain > compiler optimizations, so we should support them. The whole design and motivation behind SE-0117 is that it is not rare to want classes to be freely subclass-able internally while being closed to subclassing externally. Do you have arguments to contradict the lengthy points mentioned in the SE-0117 motivation section? The reasons for the rarity above do not make much sense to me: "it must first of all be using classes rather than a protocol with conforming structs”: we are discussing the merits of the different forms of subclassing prevention (public vs final), so I don’t see the reason for mentioning structs. "it must have a hierarchy with a parent class that is exposed outside the module”: I don’t see why it requires a hierarchy. You might want the soft-default of public to apply to non-subclassed classes which you might subclass in a later version of the library. > Currently, the spelling for a closed class is ‘public’. This makes it very > easy for library authors to create them. However, since they are a niche > feature and most of the time ‘final’ is a better choice, we do not need to > dedicate the entire ‘public’ keyword to them. > > Moreover, object-oriented programming is just as much a first-class citizen > in Swift as protocol-oriented programming is, so we should treat it as such. > Classes are inherently inheritable: when one writes “class Foo {}”, then Foo > has a default visibility of ‘internal’, and by default it can have > subclasses. That is a straightforward model, and it is easy to work with. > > If subclasses are to be disallowed, then Foo should be marked ‘final’; if Foo > is exported to clients then it should be marked ‘public’; and if both are > true then Foo should be ‘public final’. This covers all the common cases, and > leaves only the narrow corner of closed classes to consider. Per the > motivation of SE-0117, that case is worth handling. Per our collective > experience with Swift 3, however, it is not worth the added complexity of its > own access modifier keyword. We need a better way to spell it. > > One of the reasons ‘public’ was previously chosen for closed classes is to > provide a “soft default” for library authors, so they can prevent subclassing > until they decide later whether to allow it in a future release. This is a > misguided decision, as it prioritizes the convenience of library authors over > the productivity of application developers. Library authors have a > responsibility to decide what interfaces they present, and we should not > encourage them to release libraries without making those decisions. > > Moreover, we need to trust client programmers to make reasonable choices. If > a library mistakenly allows subclassing when it shouldn’t, all a client has > to do to work with it correctly is *not make subclasses*. The library is > still usable. Conversely, if a library mistakenly prohibits subclassing, then > there are things a client *should* be able to do but cannot. The harm to the > users of a library is greater in this last case, because the ability to use > the library is compromised, and that diminishes their productivity. > > We should not make “soft defaults” that tend to negatively impact the clients > of a library for the dubious benefit of enabling its author to procrastinate > on a basic design decision. If someone truly wants to publish a library with > a closed class, then we should support that. But it should be an intentional > decision, not a default. That’s a bit harsh. For me, it has nothing to do with procrastination, but with being safe-by-default, which is an important Swift goal. You are arguing for useful-by-default, which is only a different priority. > Motivation – Rethinking ‘final’ > > The question then comes to spelling. It is evident that preventing subclasses > is closely related to being ‘final’. One possibility, then, is to allow the > ‘final’ keyword to take a parameter. The parameter would be an access level, > to indicate that the type acts like it is final when accessed from at or > above that level. > > In particular, ‘final(public)’ would mean “this class cannot be subclassed > from outside the module”, or in other words “this class appears final > publicly, although it is nonfinal internally”. This approach is more powerful > than a ‘closed’ keyword because it also allows ‘final(internal)’, meaning > “this class appears final to the rest of the module, although it can be > subclassed privately”. As you can guess, I’m not a fan of this change :) I know that final has a weird place in the language now, but I’m relatively happy with the public/open design. > Motivation – Rethinking ‘private’ > > Now let us return to ‘private’, which as discussed earlier should be the only > modifier that is tighter than ‘internal’. The purpose of ‘private’ is to > enable encapsulation of related code, without revealing implementation > details to the rest of the module. It should be compatible with using > extensions to build up types, and it should not encourage overly-long files. > > The natural definition, therefore, is that ‘private’ should mean “visible in > a small group of files which belong together as a unit”. Of course Swift does > not yet have submodules, and is not likely to gain them this year. However, > if we say that each file is implicitly its own submodule unless otherwise > specified, then the model works. In that view, ‘private’ will mean “visible > in this submodule”, and for the time being that is synonymous with “visible > in this file”. This is very counterintuitive for me. I’m a proponent for going back to a file-based private but your solution has two big disadvantages for me: it makes impossible to have a file-based scope in submodules private would have different semantics depending if its in a submodule or not. As a consequence: it would be confusing to learn and use it would make copy-pasting/moving files from a submodule to a top-module potentially breaking: x.swift in submodule A class X { private init() {} } y.swift in submodule A class Y { let x = X() } This above code would stop working when moved to a top-module scope because X’s initializer would become un-accessible to Y. That’s just one example, but I’d really like to stress that changing the scope of an access-level depending on the submodule context seems like a really bad idea to me. > Although this does not immediately enable lengthy files to be separated along > natural divisions, it does lay the groundwork to allow doing so in the future > when submodules arrive. > > Motivation – Summary > > By looking at access control in its entirety, we can adopt a system that > empowers both library authors and client programmers to organize their code > in a principled way, and to expose the interfaces they want in the places > they need. The complexity of the Swift 3 visibility story, which many people > now regret creating, will be replaced by a far simpler model which in several > respects is even more powerful. > > Notably, being able to parameterize ‘final’ lets classes be closed not just > externally, but also in the rest of the module outside the ‘private’ scope if > desired. Furthermore, defining ‘private’ as being scoped to a group of > related files means that, as soon as we get the ability to create such > groups, it will no longer be necessary to write large files just to keep > implementation details hidden. > > Recommendations > > To recap, the ideas presented here focus on simplifying access control while > still supporting important use cases such as closed class hierarchies. The > indicated design uses just three familiar access keywords: > > ‘private’, to restrict visibility to a group of files, or just one file until > we get that capability. > > ‘internal’, which is the default and does not have to be written, for > module-wide visibility. > > ‘public’, to make visible outside the module, including the ability to > subclass. > > Additionally, the design allows ‘final’ to take any one of those visibility > levels as a parameter, to indicate that the type should be treated as ‘final’ > at and above the specified scope. Thus ‘final(public)’ prevents subclassing > outside the module, while ‘final(internal)’ prevents it outside the ‘private’ > scope. For consistency, ‘final(private)’ is also permitted, although it means > the same thing as ‘final’ by itself. > > Conclusion > > The Swift 3 access situation is harmful—as evidenced by the myriad calls to > fix it—not just because of its excessive complexity, but also because it > prioritizes convenience for library authors over utility for their clients, > and because it has no natural way to accommodate splitting large files into > smaller ones while preserving encapsulation. > > We have an opportunity now to correct a mistake we have made, and to set a > precedent that we *will* correct our mistakes, rather than continue down an > undesirable path simply because it seemed like a good idea at the time. When > real-world experience demonstrates that a change has taken us in the wrong > direction, we can and should update our decisions based on that new > experience. > > Therefore, in the situation at hand, we should reconsider our access modifier > story and choose a model which is both simple and powerful. I have presented > here my best efforts at describing such a system, and I offer it as one > possible way to move forward. > > > – Nevin > _______________________________________________ > swift-evolution mailing list > [email protected] > https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
