From a8b3d293b9ba44c452fc9c247140d9a4e674ae4e Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Fri, 6 Sep 2024 18:21:25 -0700 Subject: [PATCH 1/3] Draft 1 of SuppressedAssociatedTypes --- proposals/NNNN-suppressed-associated-types.md | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 proposals/NNNN-suppressed-associated-types.md diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md new file mode 100644 index 0000000000..b4ec8da332 --- /dev/null +++ b/proposals/NNNN-suppressed-associated-types.md @@ -0,0 +1,236 @@ +# Suppresssed Associated Types + +* Proposal: [SE-NNNN](NNNN-filename.md) +* Authors: [Kavon Farvardin](https://github.com/kavon), [Slava Pestov](https://github.com/slavapestov) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: on `main`, using `-enable-experimental-feature SuppressedAssociatedTypes` +* Previous Proposal: [SE-427: Noncopyable Generics](0427-noncopyable-generics.md) + +## Introduction + +When defining an associated type within a protocol, there should be a way to +permit noncopyable types as a witness. This would allow for the definition of +protocols that operate on a generic type that is not required to be `Copyable`: + +```swift +// Queue has no reason to require Element to be Copyable. +protocol Queue { + associatedtype Element + + mutating func push(_: consuming Element) + mutating func pop() -> Element +} +``` + +This creates a problem using the `Queue` protocol as an abstraction over a queue +of noncopyable elements, because the `associatedtype Element` implicitly +requires its type witness to be Copyable. + +```swift +struct WorkItem: ~Copyable { /* ... */ } + +class WorkQueue: Queue { +// `- error: type 'WorkQueue' does not conform to protocol 'Queue' + typealias Element = WorkItem +// `- note: possibly intended match 'WorkQueue.Element' (aka 'WorkItem') does not conform to 'Copyable' + + func push(_ elm: consuming Element) { /* ... */ } + func pop() -> Element? { /* ... */ } +} +``` + +There is no workaround for this problem; protocols simply cannot be used in this +situation! + +## Proposed solution + +A simple design for suppressed associated types is proposed. A protocol's +associated type that does not require a copyable type witness must be annotated +with `~Copyable`: + +```swift +protocol Manager { + associatedtype Resource: ~Copyable +} +``` + +A protocol extension of `Manager` does _not_ carry an implicit +`Self.Resource: Copyable` requirement: + +```swift +extension Manager { + func f(resource: Resource) { + // `resource' cannot be copied here! + } +} +``` + +Thus, the default conformance in a protocol extension applies only to `Self`, +and not the associated types of `Self`. For this reason, while adding +`~Copyable` to the inheritance clause of a protocol is a source-compatible +change, the same with an _associated type_ is __not__ source compatible. +The designer of a new protocol must decide which associated types are +`~Copyable` up-front. + +## Detailed design + +Requirements on associated types can be written in the associated type's +inheritance clause, or in a `where` clause, or on the protocol itself. As +with ordinary requirements, all three of the following forms define the same +protocol: +```swift +protocol P { associatedtype A: ~Copyable } +protocol P { associatedtype A where A: ~Copyable } +protocol P where A: ~Copyable { associatedtype A } +``` + +If a base protocol declares an associated type with a suppressed conformance +to `Copyable`, and a derived protocol re-states the associated type, a +default conformance is introduced in the derived protocol, unless it is again +suppressed: + +```swift +protocol Base { + associatedtype A: ~Copyable + func f() -> A +} + +protocol Derived: Base { + associatedtype A /* : Copyable */ + func g() -> A +} +``` + +Finally, conformance to `Copyable` cannot be conditional on the copyability of +an associated type: +```swift +struct ManagerManager: ~Copyable {} +extension ManagerManager: Copyable where T.Resource: Copyable {} // error +``` + +## Source compatibility + +The addition of this feature to the language does not break any existing code. + +## ABI compatibility + +The ABI of existing code is not affected by this proposal. Changing existing +code to make use of `~Copyable` associated types _can_ break ABI. +TODO: how (??) + +## Implications on adoption + +Using the feature to mark an associated type as `~Copyable` risks breaking existing source code using that protocol and ABI. + +For example, suppose the following `Queue` protocol existed before, but has now +had `~Copyable` added to the `Element`: + +```swift +public protocol Queue { + associatedtype Element: ~Copyable // <- newly added ~Copyable + + // Checks for a front element and returns it, without removal. + func peek() -> Element? + + // Removes and returns the front element. + mutating func pop() throws -> Element + + // Adds an element to the end. + mutating func push(_: consuming Element) +} +``` + +Any existing code that worked with generic types that conform to `Queue` could +show an error when attempting to copy the elements of the queue: + +```swift +// error: parameter of noncopyable type 'Q.Element' must specify ownership +func fill(queue: inout Q, + with element: Q.Element, + times n: Int) { + for _ in 0..(queue: inout Q, + with element: Q.Element, + times n: Int) + where Q.Element: Copyable { + // same as before +} +``` + +This strategy is only appropriate when all users can easily update their code. + +> NOTE: Adding the `where` clause will also help preserve the ABI of functions +> like `fill`, because without it, the new _absence_ of a Copyable requirement +> on the `Q.Element` will be mangled into the symbol for that generic function. +> +> In addition, without the `where` clause, the parameter `element` would require +> some sort of ownership annotation. Adding ownership for parameters can break +> ABI. See [SE-0377](0377-parameter-ownership-modifiers.md) for details. + +### Strategy 2: Introduce a new base protocol instead + +Rather than annotate the existing `Queue`'s associated type to be noncopyable, +introduce a new base protocol `BasicQueue` that `Queue` now inherits from: + +```swift +public protocol BasicQueue { + associatedtype Element: ~Copyable + + // Removes and returns the front element. + mutating func pop() throws -> Element + + // Adds an element to the end. + mutating func push(_: consuming Element) +} + +public protocol Queue: BasicQueue { + associatedtype Element + + // Checks for a front element and returns it, without removal. + func peek() -> Element? +} +``` + +There are two major advantages of this approach. First, users of `Queue` do not +need to update their source code. Second, any method or property requirements +that cannot be satisfied by conformers can remain in the derived protocol. + +In this example, the `peek` method requirement cannot be realistically +satisfied by an implementation of `BasicQueue` that holds noncopyable elements. +It requires the ability to return a copy of the same first element each time +it is called. Thus, it remains in `Queue`, which is now derived from the +`BasicQueue` that holds the rest of the API that _is_ compatible with +noncopyable elements. + +This strategy is only appropriate if the new base protocol can stand on its own +as a useful type to implement and use. + +> NOTE: introducing a new inherited protocol to an existing one will break ABI +> compatability. + +## Future directions + +TODO: Describe the typealias idea. + +## Alternatives considered + +TODO: explain the various ideas we've had + +## Acknowledgments + +TODO: thank people From a815eb9747d8bf6d0a27359f4625f618dc2e4cea Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 11 Sep 2024 21:04:36 -0700 Subject: [PATCH 2/3] Add some Future Directions --- proposals/NNNN-suppressed-associated-types.md | 114 +++++++++++++++++- 1 file changed, 108 insertions(+), 6 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index b4ec8da332..39305fd40f 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -117,7 +117,8 @@ The addition of this feature to the language does not break any existing code. The ABI of existing code is not affected by this proposal. Changing existing code to make use of `~Copyable` associated types _can_ break ABI. -TODO: how (??) + +TODO: how, exactly (??) ## Implications on adoption @@ -182,7 +183,7 @@ This strategy is only appropriate when all users can easily update their code. > some sort of ownership annotation. Adding ownership for parameters can break > ABI. See [SE-0377](0377-parameter-ownership-modifiers.md) for details. -### Strategy 2: Introduce a new base protocol instead +### Strategy 2: Introduce a new base protocol Rather than annotate the existing `Queue`'s associated type to be noncopyable, introduce a new base protocol `BasicQueue` that `Queue` now inherits from: @@ -221,15 +222,116 @@ This strategy is only appropriate if the new base protocol can stand on its own as a useful type to implement and use. > NOTE: introducing a new inherited protocol to an existing one will break ABI -> compatability. +> compatibility. It is equivalent to adding a new requirement on Self in the +> protocol, which can impact the mangling of generic signatures into symbols. + + + ## Future directions -TODO: Describe the typealias idea. +The future directions for this proposal are machinery to aid in the +adoption of noncopyable associated types. This is particularly relevant for +Standard Library types like Collection. + +#### Conditional Requirements + +Suppose we could say that a protocol's requirement only needs to be witnessed +if the associated type were Copyable. Then, we'd have a way to hide specific requirements of an existing protocol if they aren't possible to implement: + +```swift +public protocol Queue { + associatedtype Element: ~Copyable + + // Only require 'peek' if the Element is Copyable. + func peek() -> Element? where Element: Copyable + + mutating func pop() throws -> Element + mutating func push(_: consuming Element) +} +``` + +This idea is similar optional requirements, which are only available to +Objective-C protocols. The difference is that you statically know whether a +generic type that conforms to the protocol will offer the method. Today, this +is not possible at all: + +```swift +protocol Q {} + +protocol P { + associatedtype A + func f() -> A where A: Q + // error: instance method requirement 'f()' cannot add constraint 'Self.A: P' on 'Self' +} +``` -## Alternatives considered +#### Retroactive Protocol Inheritance + +Even if the cost of introducing a new protocol is justified, it is still an +ABI break to introduce a new inherited protocol to an existing one. +That's for good reason: a library author may add new requirements that are +unfulfilled by existing users, and that should result in a linking error. + +However, it might be possible to allow "retroactive" protocol inheritance, which +adds the inheritance along with default implementations of all inherited +requirements: + +```swift +protocol NewQueue { + associatedtype Element: ~Copyable + // ... push, pop ... +} + +protocol Queue { + associatedtype Element + // ... push, pop, peek ... +} + +// A type conforming to Queue also conforms to NewQueue where Element: Copyable. +extension Queue: NewQueue { + typealias Element = Queue.Element + mutating func push(_ e: consuming Element) { Queue.push(e) } + mutating func pop() -> Element throws { try Queue.pop() } +} +``` -TODO: explain the various ideas we've had +To make this work, this retroactively inherited protocol: + 1. Needs to provide default implementations of all requirements. + 2. Take lower precedence than a conformance to `NewQueue` declared directly on the type that conforms to `Queue`. + 3. Perhaps needs to be limited to being declared in the same module that defines the extended protocol. + +The biggest benefit of this capability is that it provides a way for all +existing types that conform to `Queue` to also work with new APIs that are based +on `NewQueue`. It is a general mechanism that works for scenarios beyond the +adoption of noncopyable associated types. ## Acknowledgments From c735bc1b3f10a3842b117823015c382b17cff21f Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Thu, 12 Sep 2024 00:19:25 -0700 Subject: [PATCH 3/3] replace established 'retroactive' term with 'bonus' --- proposals/NNNN-suppressed-associated-types.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 39305fd40f..3ca3a57fff 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -293,16 +293,15 @@ protocol P { } ``` -#### Retroactive Protocol Inheritance +#### Bonus Protocol Conformances Even if the cost of introducing a new protocol is justified, it is still an ABI break to introduce a new inherited protocol to an existing one. That's for good reason: a library author may add new requirements that are unfulfilled by existing users, and that should result in a linking error. -However, it might be possible to allow "retroactive" protocol inheritance, which -adds the inheritance along with default implementations of all inherited -requirements: +However, it might be possible to allow "bonus" protocol conformances, which +adds an extra conformance to any type that conforms to some other protocol: ```swift protocol NewQueue { @@ -316,6 +315,7 @@ protocol Queue { } // A type conforming to Queue also conforms to NewQueue where Element: Copyable. +// This is a "bonus" conformance. extension Queue: NewQueue { typealias Element = Queue.Element mutating func push(_ e: consuming Element) { Queue.push(e) } @@ -323,8 +323,8 @@ extension Queue: NewQueue { } ``` -To make this work, this retroactively inherited protocol: - 1. Needs to provide default implementations of all requirements. +To make this work, this bonus protocol conformance: + 1. Needs to provide implementations of all requirements in the bonus protocol. 2. Take lower precedence than a conformance to `NewQueue` declared directly on the type that conforms to `Queue`. 3. Perhaps needs to be limited to being declared in the same module that defines the extended protocol.