[swift-evolution] [Pitch] consistent public access modifiers

Adrian Zubarev adrian.zubarev at devandartist.com
Sat Feb 11 08:37:47 CST 2017


I have to correct myself here and there.

… which would be extensible if that feature might be added to swift one day.

Again, I see open only as a contract to allow sub-typing, conformances and overriding to the client, where extensibility of a type a story of it’s own.


-- 
Adrian Zubarev
Sent with Airmail

Am 11. Februar 2017 um 15:33:17, Adrian Zubarev (adrian.zubarev at devandartist.com) schrieb:

It wasn’t my intention to drive to far way off topic with this. The major point of my last bike shedding was that I have to disagree with you about the potential future open enum vs. public enum and closed enum.

public today does not add any guarantee to prevent the client from extending your type. For instance:

// Module A
public class A { public init() {} }

// Module B
extension A {
      
    convenience init(foo: Int) {
        print(foo)
        self.init()
    }
}
That also implies to me that open as an access modifier does not prevent extensibility.

Speaking of opened enums, we really do not mean open enum to allow extensibility where closed enum would mean the opposite. closed or @closed by all the definitions I’ve read so far is what the current public means for enums. If this is going to be fixed to closed enum (@closed public enum) than what we’re currently speaking of is nothing else than public enum, which would be extensible if that future might be added to swift one day.

Again, I see open only as a contract to prevent sub-typing, conformances and overriding, where extensibility of a type a story of it’s own.

Quickly compared to protocols: public-but-not-open protocol from module A should remain extensible in module B. Consistently that would mean that public enum is the enum when we’re talking about future extensibility of that enum from the clients side outside your module. You simply should be able to add new cases directly to your enum if it’s not annotated as closed. open enum on the other hand makes only sense when we’d speak about sub-typing on enums or value types in general.



-- 
Adrian Zubarev
Sent with Airmail

Am 11. Februar 2017 um 14:08:02, Matthew Johnson (matthew at anandabits.com) schrieb:



Sent from my iPad

On Feb 11, 2017, at 4:25 AM, Adrian Zubarev via swift-evolution <swift-evolution at swift.org> wrote:

I’m probably better describing things with some bikeshedding code, but feel free to criticize it as much as you’d like.

//===========--------- Module A ---------===========//
@closed public enum A {
    case a
}

extension A {
    case aa // error, because enum is closed
}
This is an error because you can't add cases in an extension.  I imagine this is how cases would be added outside the module if we allow `open enum` in the future.  But whether or not this is allowed *within* the module is a separate question that is orthogonal to `closed` and `open`.


  
public func foo(a: A) {
    switch a {
    case .a:
        print("done")
    }
}

public enum B {
    case b
}

extension B {
    case bb // fine, because not-closed enums are extensible
}
As noted above, whether this is allowed or not *within* the module is orthogonal to `closed`.  *Outside* the module it would only be possible for enum declared `open` (if we add this feature in the future).

  
public func bar(b: B) {
    switch b {
    case .b:
        print("b")

    default: // always needed
        print("some other case")
    }
}

// Sub-enum relationships

// Possible even the enum A is closed, because `@closed` only    
// closes the extensibility of an enum
enum SubA : A {
    case aa
}

Now you're talking about value subtypes.  That is orthogonal.  Also, this syntax already has a meaning (the raw value of the enum is A) so we wouldn't be able to use it the way you are intending here.  Finally, it is misleading syntax because what you mean here is "A is a subtype of SubA" which is exactly the opposite of what the syntax implies.

All values of A are valid values of SubA, but SubA has values that are not valid values of A.

 // The following enum can have a sub-enum in the clients module
open enum C {
    case c
}
public func cool(c: C) {
    switch c {
    case .c:
        print("c")

    default: // always needed
        print("some other case")
    }
}

@closed open enum D {
    case d
}

public func doo(d: D) {
    switch b {
    case .b:
        print("b")
    }
}

// The enum case is always known at any point, no matter    
// where the instance comes from, right?

let subA = SubA.aa
let otherSubA = SubA.a // Inherited case

let a: A = subA        // error, downgrade the sub-enum to A first
let a: A = otherSubA   // okay

foo(a: subA)           // error, downgrade the sub-enum to A first
foo(a: otherSubA)      // okay

//===========--------- Module B ---------===========//

// Totally fine    
switch A.a {
case .a:
    print("done")
}

extension A {
    case aa // not allowed because the enum is closed
}

extension B {
    case bbb
}

switch B.b {
case .b:
    print("b")
default:    
    print("somethine else")
}

bar(b: B.bbb) // fine, because the switch statement on enums without    
// `@closed` has always`default`

// Allowed because `C` is open, and open allows sub-typing, conforming    
// and overriding to the client
enum SubC : C {
    case cc
}

let subC = SubC.cc

cool(c: subC) // okay

enum SubD : D {
    case dd
}

doo(d: D.dd)// error, downgrade sub-enum to D first
My point here is, that we should not think of (possible) open enums as enums that the client is allowed to extend. That way we’re only creating another inconsistent case for the open access modifier. As far as I can tell, open as for today means “the client is allowed to subclass/override things from a different module”.

Yes, but subclasses are analogous to enum cases.  A subtype of an enum would remove cases.  I think you are misunderstanding the relationship of enums to classes and protocols.

And I already said it hundred of times that we should extend this to make open a true access modifier in Swift. That said the meaning of open should become:

The client is allowed to sub-type (currently only classes are supported).
The client is allowed to conform to open protocols
The client is allowed to override open type members
This also means that extensibility is still allowed to public types. Public-but-not-open classes are still extensible today, which is the correct behavior. Extending an enum which is not closed could or probably should be made possible through extensions, because I cannot think of anther elegant way for the client to do so.

This is what `open enum` would allow.  It is the proper enum analogue of open classes.

That will leave us the possibility to think of sub-typing enums in the future (I sketched it out a little above).

Value subtyping is very interesting.  I have been working on some ideas around this but I want to keep this thread focused.

If I’m not mistaken, every enum case is known at compile time,

This is true today but will not always be true in the future.  That is in large part what this thread is about.

which means to me that we can safely check the case before allowing to assign or pass an instance of a sub-enum to some of its super-enum. (Downgrading an enum case means that you will have to write some code that either mutates your current instance or creates a new one which matches one of the super-enum cases.) Furthermore that allows a clear distinction of what open access modifier does and how @closed behaves.

I'm not going to comment on the rest because it is premised on a misunderstanding of what value subtyping is.  I'm going to share some ideas around value subtyping in a new thread as soon as I have a chance to finish putting them together.

To summarize:

@closed enum - you’re not allowed to add new cases to the enum in your lib + (you’re allowed to create sub-enums)
@closed public enum - you and the client are not allowed to add new cases (+ the client is not allowed to create sub-enums)
@closed open enum - you and the client are not allowed to add new cases (+ the client might create new sub-enums)
enum - you’re allowed to add new cases (default is needed in switch statements) (+ you can create new sub-enums)
public enum - you and the client are allowed to add new cases (+ only you are allowed to create new sub-enums)
open enum - you and the client are allowed to add new cases (everyone can create new sub-enums)
This is a lot of bike shedding of mine, and the idea might not even see any light in Swift at all, but I’d like to share my ideas with the community. Feel free to criticize them or flesh something out into something real. :)

P.S.: If we had something like this:

@closed enum X {
    case x, y
    func foo() {
     switch self {
        case .x, .y:
            print("swift")
    }
}

enum Z : X {
    case z, zz
    override func foo() {
        // Iff `self` is `z` or `zz` then calling super will result in an error.
        // Possible solution: always tell the client to downgrade explicitly the    
        // case first if there is an attempt to call super (if mutating),    
        // or handle all cases

        switch self {
        case .z, .zz:
            print("custom work")
        default: // or all super-enum cases
            super.foo()
        }
    }
}


-- 
Adrian Zubarev
Sent with Airmail

Am 11. Februar 2017 um 04:49:11, Xiaodi Wu via swift-evolution (swift-evolution at swift.org) schrieb:

On Wed, Feb 8, 2017 at 5:05 PM, Matthew Johnson via swift-evolution <swift-evolution at swift.org> wrote:
I’ve been thinking a lot about our public access modifier story lately in the context of both protocols and enums.  I believe we should move further in the direction we took when introducing the `open` keyword.  I have identified what I think is a promising direction and am interested in feedback from the community.  If community feedback is positive I will flesh this out into a more complete proposal draft.


Background and Motivation:

In Swift 3 we had an extended debate regarding whether or not to allow inheritance of public classes by default or to require an annotation for classes that could be subclassed outside the module.  The decision we reached was to avoid having a default at all, and instead make `open` an access modifier.  The result is library authors are required to consider the behavior they wish for each class.  Both behaviors are equally convenient (neither is penalized by requiring an additional boilerplate-y annotation).

A recent thread (https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170206/031566.html) discussed a similar tradeoff regarding whether public enums should commit to a fixed set of cases by default or not.  The current behavior is that they *do* commit to a fixed set of cases and there is no option (afaik) to modify that behavior.  The Library Evolution document (https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst#enums) suggests a desire to change this before locking down ABI such that public enums *do not* make this commitment by default, and are required to opt-in to this behavior using an `@closed` annotation.

In the previous discussion I stated a strong preference that closed enums *not* be penalized with an additional annotation.  This is because I feel pretty strongly that it is a design smell to: 1) expose cases publicly if consumers of the API are not expected to switch on them and 2) require users to handle unknown future cases if they are likely to switch over the cases in correct use of the API.

The conclusion I came to in that thread is that we should adopt the same strategy as we did with classes: there should not be a default.

There have also been several discussions both on the list and via Twitter regarding whether or not we should allow closed protocols.  In a recent Twitter discussion Joe Groff suggested that we don’t need them because we should use an enum when there is a fixed set of conforming types.  There are at least two  reasons why I still think we *should* add support for closed protocols.

As noted above (and in the previous thread in more detail), if the set of types (cases) isn’t intended to be fixed (i.e. the library may add new types in the future) an enum is likely not a good choice.  Using a closed protocol discourages the user from switching and prevents the user from adding conformances that are not desired.

Another use case supported by closed protocols is a design where users are not allowed to conform directly to a protocol, but instead are required to conform to one of several protocols which refine the closed protocol.  Enums are not a substitute for this use case.  The only option is to resort to documentation and runtime checks.


Proposal:

This proposal introduces the new access modifier `closed` as well as clarifying the meaning of `public` and expanding the use of `open`.  This provides consistent capabilities and semantics across enums, classes and protocols.

`open` is the most permissive modifier.  The symbol is visible outside the module and both users and future versions of the library are allowed to add new cases, subclasses or conformances.  (Note: this proposal does not introduce user-extensible `open` enums, but provides the syntax that would be used if they are added to the language)

`public` makes the symbol visible without allowing the user to add new cases, subclasses or conformances.  The library reserves the right to add new cases, subclasses or conformances in a future version.

`closed` is the most restrictive modifier.  The symbol is visible publicly with the commitment that future versions of the library are *also* prohibited from adding new cases, subclasses or conformances.  Additionally, all cases, subclasses or conformances must be visible outside the module.

Note: the `closed` modifier only applies to *direct* subclasses or conformances.  A subclass of a `closed` class need not be `closed`, in fact it may be `open` if the design of the library requires that.  A class that conforms to a `closed` protocol also need not be `closed`.  It may also be `open`.  Finally, a protocol that refines a `closed` protocol need not be `closed`.  It may also be `open`.

This proposal is consistent with the principle that libraries should opt-in to all public API contracts without taking a position on what that contract should be.  It does this in a way that offers semantically consistent choices for API contract across classes, enums and protocols.  The result is that the language allows us to choose the best tool for the job without restricting the designs we might consider because some kinds of types are limited with respect to the `open`, `public` and `closed` semantics a design might require.


Source compatibility:

This proposal affects both public enums and public protocols.  The current behavior of enums is equivalent to a `closed` enum under this proposal and the current behavior of protocols is equivalent to an `open` protocol under this proposal.  Both changes allow for a simple mechanical migration, but that may not be sufficient given the source compatibility promise made for Swift 4.  We may need to identify a multi-release strategy for adopting this proposal.

Brent Royal-Gordon suggested such a strategy in a discussion regarding closed protocols on Twitter:

* In Swift 4: all unannotated public protocols receive a warning, possibly with a fix-it to change the annotation to `open`.
* Also in Swift 4: an annotation is introduced to opt-in to the new `public` behavior.  Brent suggested `@closed`, but as this proposal distinguishes `public` and `closed` we would need to identify something else.  I will use `@annotation` as a placeholder.
* Also In Swift 4: the `closed` modifier is introduced.

* In Swift 5 the warning becomes a compiler error.  `public protocol` is not allowed.  Users must use `@annotation public protocol`.
* In Swift 6 `public protocol` is allowed again, now with the new semantics.  `@annotation public protocol` is also allowed, now with a warning and a fix-it to remove the warning.
* In Swift 7 `@annotation public protocol` is no longer allowed.

A similar mult-release strategy would work for migrating public enums.

A different line of feedback here:

As per previous reply, I now think if we clarify the mental model of the access modifier hierarchy you're proposing and adopt or reject with that clarity, we'll be fine whether we go with `closed` or with `@closed`. But I don't think the source compatibility strategy you list is the most simple or the most easy to understand for end users.

- I'll leave aside closed protocols, which as per Jordan Rose's feedback may or may not have sufficient interestingness.
- With respect to enums, I don't think we need such a drastic whiplash in terms of what will compile in future versions. Instead, we could take a more pragmatic approach:

1. In Swift 4, remove the warning (or is it error?) about `default` cases in switch statements over public enums. Simultaneously, add `closed` or `@closed` (whatever is the approved spelling) and start annotating standard library APIs. The annotation will be purely future-proofing and have no functional effect (i.e. the compiler will do nothing differently for a `closed enum` or `@closed public enum` (as the case may be) versus a plain `public enum`).
2. In Swift 4.1, _warn_ if switch statements over public enums don't have a `default` statement: offer a fix-it to insert `default: fatalError()` and, if the enum is in the same project, offer a fix-it to insert `closed` or `@closed`.
3. In Swift 5, upgrade the warning to an error for non-exhaustiveness if a switch statement over a public enum doesn't have a `default` statement. Now, new syntax to extend an `open enum` can be introduced and the compiler can treat closed and public enums differently.

_______________________________________________
swift-evolution mailing list
swift-evolution at swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution at swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170211/84330127/attachment.html>


More information about the swift-evolution mailing list