> On Feb 11, 2017, at 7:56 AM, Karl Wagner <[email protected]> wrote:
>
>
>> On 11 Feb 2017, at 14:37, Karl Wagner <[email protected]
>> <mailto:[email protected]>> wrote:
>>
>>
>>> On 9 Feb 2017, at 00:05, Matthew Johnson via swift-evolution
>>> <[email protected] <mailto:[email protected]>> 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
>>>
>>> <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
>>> <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.
>>>
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> [email protected] <mailto:[email protected]>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
>>
>> A couple of points:
>>
>> 1) Protocols can also be sub-typed as well as conformed to. “Open” for
>> classes refers to sub-typing. As I understand it, you would not be able to
>> derive a protocol from a non-open protocol. Perhaps worth mentioning.
>>
>
> Actually now that I read that again I’m not sure. People use marker protocols
> to group several protocol requirements, and to write extensions on
> collections of types based on those collections of requirements. For example:
>
> // Module 1
>
> closed protocol DoesSomething {
> func doSomething()
> }
>
> struct MyTypeOne: Collection, DoesSomething { /* … */ }
> struct MyTypeTwo: Collection, DoesSomething { /* … */ }
> enum MyTypeThree: DoesSomething { /* … */ }
>
> // Module 2
>
> // Should this be allowed? Does it inherit the “closed-ness” of DoesSomething?
> protocol CollectionWhichDoesSomething: Collection, DoesSomething {
> /* Empty, marker protocol */
> }
>
> // Since MyType{One, Two} already conform to DoesSomething, this doesn’t
> introduce a new conformance to the closed protocol.
> // Should it be allowed, then?
> extension MyTypeOne: CollectionWhichDoesSomething {}
> extension MyTypeTwo: CollectionWhichDoesSomething {}
>
> This could probably get better with better existential support, but write now
> we can’t use or extend a “Collection & DoesSomething” existential. You have
> to wrap those requirements in a protocol and add conformances to it, and you
> might need to do that for closed protocols, too. As long as you don’t
> introduce new conformances to that closed protocol, nothing in Module 1 would
> break.
Yes, as I stated in my previous reply, there is no reason to disallow this. A
library may have good reasons for not allowing clients to add conformances. I
can’t think of any good reason why it would want to restrict refinements of its
protocols.
>
>> 2) Enums are conceptually closed - that’s the whole point behind requiring
>> exhaustive switches. Allowing later library versions to add/remove cases is
>> important, but adding default cases to handle case “?” sounds like a bad
>> solution. With zero information about what is happening, and the chance that
>> some cases may not exist any more, anything you write in such a default
>> statement would be useless and nonsensical.
>>
>> I think API versioning is the better way to deal with this. Version 3 of
>> MyLibrary.MyEnum has cases { A, B, C }, and Version 3.1 has cases { A, C, D,
>> E }. I should be able to write code which handles either complete set of
>> cases, depending on which version of MyLibrary is installed.
>
_______________________________________________
swift-evolution mailing list
[email protected]
https://lists.swift.org/mailman/listinfo/swift-evolution