To begin with, I'm not a fan of `protected` access. But even leaving that 
aside, I have a few questions and critiques.

> A common case is the UIView from UIKit. Many developers are tempted to make 
> this call:
> 
> view.layoutSubviews()
> The documentation says: "You should not call this method directly. If you 
> want to force a layout update, call the setNeedsLayoutmethod instead to do so 
> prior to the next drawing update. If you want to update the layout of your 
> views immediately, call the layoutIfNeeded method."

This example is illuminating in several ways.

* The rule is not simply that "only the class should call `layoutSubviews()`"; 
it is effectively "*you* should never call `layoutSubviews()` except when 
`super`ing up from your override". Calling `layoutSubviews()` from 
`insertRows(at:)` is just as much a mistake if `insertRows(at:)` is part of the 
class as if it is not. So isn't `protected` insufficiently strict to properly 
serve this use case?

* At the same time, something outside `layoutSubviews()` has to be able to call 
`layoutSubviews()`. In the case of UIKit, though, that "something" is always 
within UIKit itself, never outside it. So should `protected` have a "bottom", a 
level below which calls are unrestricted? For instance, in UIKit's case you 
might have `protected fileprivate`, meaning "anything up to `fileprivate` has 
unrestricted use; anything above that can override and `super` up from its 
override, but not use it any other way".

        protected fileprivate func layoutSubviews()

* `layoutSubviews()` is also something you should probably always `super` up 
to. Have you considered addressing `super` requirements at all?

In short, is a traditional `protected` really the feature you want to handle 
this use case, or would a very different design actually suit it a lot better?

> When declarated by a class the protected member will be visible to the class 
> itself and all the derived classes.

In what scope? The same as the class?

Is there not room for, for instance, "usable without restriction in this file, 
override-only in the rest of this module, invisible outside it"? For instance, 
`internal(protected) fileprivate`, or perhaps `internal(override) fileprivate`? 
`layoutSubviews()` might then be `public(override) fileprivate`—the ability to 
override is public, the ability to use it unrestricted is filewide.

        public(override) fileprivate func layoutSubviews()
        internal(override) fileprivate func privateSubclassingHook()

> public protected(set) var x = 20

Of course, that might be difficult to combine with the `(set)` syntax. 
`public(set: override)`, maybe? With, for instance, `public internal(set: 
override) private(set)` if you want the property's getter public and its setter 
overridable internally and callable in private scope.

        public(override) fileprivate func layoutSubviews()
        internal(override) fileprivate func privateSubclassingHook()
        public(get, set: override) internal(set) var x = 20

But there's something about this that's starting to seem a little rotten. I 
think the problem is that we're not really trying to widen the ability to 
override, we're trying to restrict the ability to call. Let's try restructuring 
along those lines:

        public fileprivate(call) func layoutSubviews()
        internal fileprivate(call) func privateSubclassingHook()
        public internal(set: call) var x = 20

That seems much cleaner to me.

> If the member is declared as final then it will be visible but not can be 
> overrided by the derived classes. Just like it works with other access levels.

With the "overridable but otherwise unusable" conception I'm suggesting, this 
would not be the case, of course.

> Protocols
> 
> Protocols do not declare access level for their members. So the protected 
> access level is not applicable here.

But `protected` is quite different from other access levels; it does not limit 
the visibility of the symbols, but rather their use. And protocols face the 
same sort of problem as classes, where certain members are essentially override 
hooks and shouldn't be called directly outside a particular scope.

So I think we ought to allow `accesslevel(call)`, but not a plain `accesslevel`:

        public fileprivate(call) func layoutSubviews()
        internal fileprivate(call) func privateSubclassingHook()
        public internal(set: call) var x = 20
        internal(call) func protocolConformanceHook()
        fileprivate(set: call) var onlyProtocolSetsThis: Int { get set }

> Extensions
> 
> Extensions will not be able do be protected nor their members.

This is very vague. There are several things extensions might try to do with 
protected members:

* Declare new ones
* Override existing ones
* Call existing ones

Which of these, if any, are permitted? Why?

In my conception, I would permit extensions to behave as the type they extended 
did. Extensions could declare new members with restricted calling and override 
existing ones. They would not be able to call, except when supering from an 
override, unless they were within scope of the `call` access control. In other 
words, they'd behave just like any other code at that location. That's how we 
want extensions to work.

> But nested declarations will be allowed, so this code will compile:
> 
> // We can declare a protected class (or struct, enum, etc.) if
> // and only if they are nested inside other type.
> public class MyPublicClass {
>    protected 
> class MyProtectedClass {

What does it mean to "use" a protected class, though? Clearly you can call its 
methods, if only through AnyObject or a non-protected superclass or a protocol 
it conforms to. Does it mean you can't instantiate it? Does it mean you can't 
subclass it? Does it mean you can't call methods that aren't on its supertypes? 
All of the above? None?

One more thing that didn't come up: Testability. I believe that importing a 
module with `@testable` should disable its call restrictions, even ones 
inherited from outside that module. Thus, even if *you* cannot call your 
`layoutSubviews()`, your test suite can.

So, in short, my counter-proposal is:

        public fileprivate(call) func layoutSubviews()
        internal fileprivate(call) func privateSubclassingHook()
        public internal(set: call) var x = 20
        internal(call) func protocolConformanceHook()
        fileprivate(set: call) var onlyProtocolSetsThis: Int { get set }

In other words:

* There is a new aspect of the member, `call`, which controls the ability to 
actually call the member, as opposed to overriding it. No `call`, no calling 
(except when `super`ing up from an override).

* `call` is used in combination with one of the existing access modifiers: 
`public(call)` `internal(call)` `fileprivate(call)` `private(call)`. `call`'s 
visibility is always less or equal to the member itself.

* To control the callability of a setter independently from both the getter and 
the overridability of the setter, use `set: call`.

* Extensions behave just like type definitions at the same location with 
regards to `call`.

* Protocols can use access modifiers with `call` to prevent unauthorized code 
from calling a member. The access control level to implement a member continues 
to be as wide as the access control level of the protocol itself.

* `@testable` disables `call` restrictions on the types it imports, so the test 
suite can call any visible member, even ones inherited from other modules.

* There should probably also be some sort of "super required" warning/error, 
but this is an orthogonal feature and can be left for a separate proposal.

I think that feature will be closer to the one you actually *want*, as opposed 
to the one that other languages have cargo-culted from SIMULA-67.

-- 
Brent Royal-Gordon
Architechies

_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to