[swift-evolution] A Comprehensive Rethink of Access Levels in Swift
Nevin Brackett-Rozinsky
nevin.brackettrozinsky at gmail.com
Thu Feb 23 15:56:42 CST 2017
*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.
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.
*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”.
*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”.
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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170223/550a8041/attachment.html>
More information about the swift-evolution
mailing list