From a8b3d293b9ba44c452fc9c247140d9a4e674ae4e Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Fri, 6 Sep 2024 18:21:25 -0700 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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. From 19c307ebcfec75d0c1e10f757615b931ae7b823f Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Fri, 29 Aug 2025 13:43:30 -0700 Subject: [PATCH 04/11] add Slava's edits --- proposals/NNNN-suppressed-associated-types.md | 386 ++++++------------ 1 file changed, 126 insertions(+), 260 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 3ca3a57fff..1f4e7595d7 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -1,338 +1,204 @@ -# Suppresssed Associated Types +# Suppressed Default Conformances on 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) +* Previous Proposals: [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), [SE-446: Nonescapable Types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.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`: - +Today, it is not possible to declare an associated type that does not require its +_type witnesses_ to be `Copyable` or `Escapable`. For example, consider the `Element` +associated type of `Queue` below: ```swift -// Queue has no reason to require Element to be Copyable. -protocol Queue { +/// Queue has no reason to require Element to be Copyable. +protocol Queue: ~Copyable { associatedtype Element - mutating func push(_: consuming Element) - mutating func pop() -> Element + mutating func push(_: consuming Self.Element) + mutating func pop() -> Self.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. - +While the conforming type is itself permitted to be noncopyable, its `Element` +type witness has 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? { /* ... */ } +/// error: LinkedListQueue does not conform to Queue +struct LinkedListQueue: ~Copyable, Queue { + ... } ``` +This is an expressivity limitation in practice, and there is no workaround +possible today. -There is no workaround for this problem; protocols simply cannot be used in this -situation! +## Proposed Solution -## 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`: +We propose that the existing syntax for suppressing these conformances be +extended to associated type declarations: ```swift -protocol Manager { - associatedtype Resource: ~Copyable -} -``` - -A protocol extension of `Manager` does _not_ carry an implicit -`Self.Resource: Copyable` requirement: +/// Correct Queue protocol. +protocol Queue: ~Copyable { + associatedtype Element: ~Copyable -```swift -extension Manager { - func f(resource: Resource) { - // `resource' cannot be copied here! - } + mutating func push(_: consuming Self.Element) + mutating func pop() -> Self.Element } ``` -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. +Similarly, stating `~Escapable` should be allowed, to suppress the default conformance +to `Escapable`. + +## Detailed Design -## Detailed design +There are three ways to impose a requirement on an associated type: +- In the inheritance clause of the associated type declaration. +- In a `where` clause attached to the associated type declaration. +- In a `where` clause attached to the protocol itself. -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: +We extend the **Detailed Design** section of +[SE-427: Noncopyable Generics](0427-noncopyable-generics.md) to allow +suppressing default conformance to `Copyable` in `Escapable` in all of +the above positions. Thus, all three below are equivalent: ```swift protocol P { associatedtype A: ~Copyable } -protocol P { associatedtype A where A: ~Copyable } -protocol P where A: ~Copyable { associatedtype A } +protocol P { associatedtype A where Self.A: ~Copyable } +protocol P where Self.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: +### Protocol inheritance +This interacts with protocol inheritance as follows. If a base protocol +declares an associated type with a suppressed conformance, this +associated type will also have a suppressed conformance in the derived +protocol, unless the derived protocol re-states the associated type. That is: ```swift protocol Base { associatedtype A: ~Copyable - func f() -> A } -protocol Derived: Base { - associatedtype A /* : Copyable */ - func g() -> A +protocol Derived1: Base { + // A is still ~Copyable here } -``` -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 +protocol Derived2: Base { + // A now defaults to Copyable + associatedtype A +} ``` -## 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, exactly (??) - -## 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`: +### No recursion +Suppressed conformances on associated types differ from those on generic +parameters and protocols in one crucial respect. Here is the protocol +`Queue` from earlier: ```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) +/// Correct Queue protocol. +protocol Queue: ~Copyable { + associatedtype Element: ~Copyable + + mutating func push(_: consuming Self.Element) + mutating func pop() -> Self.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: - +Recall the existing rules from +[SE-427: Noncopyable Generics](0427-noncopyable-generics.md). Under +those rules, a protocol extension of `Queue` always introduces a +default `Self: Copyable` requirement; that is: ```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 +extension Queue where Self: ~Copyable { + ... } ``` -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 - -Rather than annotate the existing `Queue`'s associated type to be noncopyable, -introduce a new base protocol `BasicQueue` that `Queue` now inherits from: - +However, with the current proposal,this defaulting behavior does +not extend to associated types +with supressed conformances. In particular, no implicit +`Self.Element: Copyable` requirement is introduced above, by +either extension. Instead, a protocol extension +for queue types where **both** the queue itself and the element +type are `Copyable` takes the following form: ```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? +extension Queue where Self.Element: Copyable { + ... } ``` -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 is discussed further in **Source Compatibility** below. -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 -> compatibility. It is equivalent to adding a new requirement on Self in the -> protocol, which can impact the mangling of generic signatures into symbols. - - +## Source Compatibility +The introduction of this feature in the language does not break +any existing code, because any usage of the suppressed conformance +syntax with associated types was diagnosed as an error. -## Future directions - -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: +However, changing an existing associated type declaration to suppress +conformance to `Copyable` or `Escapable` is a +**source-breaking** change, as a consequence of the design +discussed in **No recursion** above. +For example, if a library publishes this protocol: ```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) +public protocol Manager: ~Copyable { + associatedtype Resource } ``` - -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: - +Client code that states a `T: Manager` requirement on a generic +parameter `T` can then assume that the type parameter +`T.Resource` is `Copyable`: ```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' +extension Manager where Self: ~Copyable { + func makeCopies(_ e: Self.Element) -> (Self.Element, Self.Element) { + return (e, e) + } } ``` - -#### 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 "bonus" protocol conformances, which -adds an extra conformance to any type that conforms to some other protocol: - +Now suppose the library author then changes the protocol to +suppress conformance: ```swift -protocol NewQueue { - associatedtype Element: ~Copyable - // ... push, pop ... +public protocol Manager: ~Copyable { + associatedtype Resource: ~Copyable } +``` +The client's extension of `Manager` will no longer type check, because +the body of `makeCopies()` assumes `e` is `Copyable`, and this +assumption is no longer true. -protocol Queue { - associatedtype Element - // ... push, pop, peek ... -} +## ABI Compatibility -// 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) } - mutating func pop() -> Element throws { try Queue.pop() } -} -``` +The ABI of existing code is not affected by this proposal. -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. - -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. +On the other hand, changing an associated type declaration in an library +to suppress conformance is an ABI-breaking change, for similar reasons +to those described above. -## Acknowledgments +## Alternatives Considered -TODO: thank people +A more advanced form of this idea would attempt to introduce "recursive +`Copyable` requirements" (and similarly for `Escapable`). This was already +discussed in the **Alternatives Considered** section of +[SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), and the difficulties outlined there still +apply today. \ No newline at end of file From 2576055242967c2826f0cc0de73d9094f3785a3c Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Fri, 29 Aug 2025 14:10:32 -0700 Subject: [PATCH 05/11] Verified this has been on main since at least Swift 6.1.2 --- proposals/NNNN-suppressed-associated-types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 1f4e7595d7..0d52eecb95 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -4,7 +4,7 @@ * 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` +* Implementation: on `main` and available since at least Swift 6.1.2, using `-enable-experimental-feature SuppressedAssociatedTypes` * Previous Proposals: [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), [SE-446: Nonescapable Types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md) ## Introduction @@ -201,4 +201,4 @@ A more advanced form of this idea would attempt to introduce "recursive `Copyable` requirements" (and similarly for `Escapable`). This was already discussed in the **Alternatives Considered** section of [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), and the difficulties outlined there still -apply today. \ No newline at end of file +apply today. From 68b37f540128d9ec90d4d3fac276575077926388 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Mon, 15 Sep 2025 11:08:34 -0700 Subject: [PATCH 06/11] s/Element/Resource/ and related typos --- proposals/NNNN-suppressed-associated-types.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 0d52eecb95..d0e6720599 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -171,8 +171,8 @@ parameter `T` can then assume that the type parameter `T.Resource` is `Copyable`: ```swift extension Manager where Self: ~Copyable { - func makeCopies(_ e: Self.Element) -> (Self.Element, Self.Element) { - return (e, e) + func makeCopies(_ r: Self.Resource) -> (Self.Resource, Self.Resource) { + return (r, r) } } ``` @@ -184,7 +184,7 @@ public protocol Manager: ~Copyable { } ``` The client's extension of `Manager` will no longer type check, because -the body of `makeCopies()` assumes `e` is `Copyable`, and this +the body of `makeCopies()` assumes `r` is `Copyable`, and this assumption is no longer true. ## ABI Compatibility From 8b9b322786ea989d34e90f62ebfaaec6f423aa5d Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Mon, 15 Sep 2025 13:44:55 -0700 Subject: [PATCH 07/11] add pitch link --- proposals/NNNN-suppressed-associated-types.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index d0e6720599..6458138f4c 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -6,6 +6,7 @@ * Status: **Awaiting review** * Implementation: on `main` and available since at least Swift 6.1.2, using `-enable-experimental-feature SuppressedAssociatedTypes` * Previous Proposals: [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), [SE-446: Nonescapable Types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md) +* Review: [Pitch](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880) ## Introduction From b9edc43edf8c1cb4a38801b5ec1ee604be83f3a1 Mon Sep 17 00:00:00 2001 From: Joe Groff Date: Fri, 3 Oct 2025 16:35:24 -0700 Subject: [PATCH 08/11] Suppressed associated types: discuss more alternatives for defaulting behavior. (cherry picked from commit bfea556db287df195b44da7ba22023f492458d42) --- proposals/NNNN-suppressed-associated-types.md | 167 +++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 6458138f4c..5b05bd54ef 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -121,7 +121,7 @@ extension Queue where Self: ~Copyable { } ``` -However, with the current proposal,this defaulting behavior does +However, with the current proposal, this defaulting behavior does not extend to associated types with supressed conformances. In particular, no implicit `Self.Element: Copyable` requirement is introduced above, by @@ -136,6 +136,51 @@ extension Queue where Self.Element: Copyable { This is discussed further in **Source Compatibility** below. +### Library evolution and new associated type requirements + +Another complication in extending the defaulting behavior of generic +parameters to associated types comes from library evolution. Protocols +are allowed to introduce new requirements, including associated type +requirements, without breaking source or binary compatibility, as long +as a default implementation is provided for existing code. +After this proposal, a new associated type can be `~Copyable` and/or +`~Escapable`, and the default type could be non-`Copyable` or +non-`Escapable`. + +```swift +protocol Foo { + // Added in v2 + associatedtype New: ~Copyable +} + +struct NC: ~Copyable {} + +// Added in v2 +extension Foo { typealias New = NC } +``` + +If the defaulting rule for generic parameters extended to all associated types, +then a protocol introducing an associated type would change the meaning of source +code when it compiles against the new definition of the protocol, since the new +associated type would impose new default requirements. This could cause existing +code to stop compiling when the default implementation of the new associated type, +used by existing conformances to the modified protocol, is non-`Copyable` or +non-`Escapable` so does not satisfy those default requirements. + +```swift +struct ExistingConformance: Foo {} + +// If `T: Foo` implied `T.New: Copyable` after recompiling against Foo v2... +func existingFunction(_: T) {} + +func existingCaller() { + // ...then this previously-working line of code would stop compiling, because + // ExistingConformance.New defaults to noncopyable type `NC`, so doesn't + // satisfy the default `T.New: Copyable` requirement. + existingFunction(ExistingConformance()) +} +``` + ### Conditional conformance Finally, recall that concrete types may conform to `Copyable` and @@ -198,8 +243,126 @@ to those described above. ## Alternatives Considered +### Recursive requirements + A more advanced form of this idea would attempt to introduce "recursive `Copyable` requirements" (and similarly for `Escapable`). This was already discussed in the **Alternatives Considered** section of [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), and the difficulties outlined there still -apply today. +apply today. + +If we were able to design and implement such a feature, it still would not +address the library evolution problem with default requirements on +associated types. + +### Definition-driven associated type defaults + +Rather than try to impose a blanket default on all associated types, we might +instead apply a limited defaulting rule only to select associated types, driven +by some aspect of the protocol definition. This could avoid the infinite recursion +and library evolution problems, if designed properly, at the expense of increased +language complexity. Readers would have to consult the definitions of protocols +to see whether they come with default `Copyable` or `Escapable` requirements. + +Some possibilities for how this might look include: + +#### Defaulting only for primary associated types + +Primary associated types have a strong correlation to what one might consider the core interface of a protocol. They also can’t be added to or removed once declared without breaking source compatibility. So we could only default primary associated types: + +``` +protocol Container { + associatedtype Element: ~Copyable & ~Escapable + associatedtype Iterator: ~Copyable & ~Escapable +} + +func foo() {...} +// defaults T: Copyable, T: Escapable, T.Element: Copyable, T.Element: Escapable +// but defaults to leaving Iterator unconstrained +``` + +In order to avoid creating an infinite set of defaults, this would not be +recursive, but only apply to + +One drawback of this approach is that it would make adding a `~Copyable` and/or +`~Escapable` associated type as a primary associated type to a protocol that +had not already declared primary associated types would become a source-breaking +change. Currently, a protocol without primary associated types can add +some without affecting compatibility with existing source code. + +#### Protocol-defined default requirements + +We could let a protocol definition dictate any set of `Copyable` or `Escapable` requirements to get imposed by default when used as a generic requirement. This set of requirements would have to be finite. + +``` +protocol Container: ~Copyable, ~Escapable { + associatedtype BorrowingIterator: BorrowingIteratorProtocol, + ~Copyable, ~Escapable + associatedtype Element: ~Copyable, ~Escapable + + default Element: Copyable, Element: Escapable +} +``` + +This might also serve as a way for a protocol to opt generic parameters out +of defaulting to `Copyable` and/or `Escapable` when the protocol is used as a constraint, +which may be desirable for protocols that are only used with non-`Copyable` or +non-`Escapable` conformers in practice. + +#### Default constraint sets + +There may be more than one local optimum set of default requirements for a protocol. An elaboration of the protocol-defined defaults idea might be to allow multiple default constraint sets, which can be individually suppressed as a group. For instance, this would make it possible to provide constraint sets to suppress copying and escaping individually, without making developers write out the entire set of constraint suppressions: + +``` +protocol Container: ~Copyable, ~Escapable { + associatedtype BorrowingIterator: BorrowingIteratorProtocol, + ~Copyable, ~Escapable + associatedtype Element: ~Copyable, ~Escapable + + default constraintset Copying where Self: Copyable, Self.Element: Copyable + default constraintset Escaping where Self: Escapable, Self.Element: Escapable +} + +// implicitly has Copying & Escaping sets of requirements +extension Container {} + +extension Container without Copying {} // some inbetween kind +extension Container without Escaping {} + +extension Container without Copying, Escaping {} // fully unconstrained in -version + +// For generic signatures in other positions, we could have syntax +// that allows you to refer to constraintsets like a member: +func f() without T: Container.Copying {} + +``` + +This functionality might also be used for future evolution. Let’s say we add a third suppressable protocol `Runcible` in the future, and we want to generalize `Container` to allow for `~Runcible` elements. We can suppress the `Runcible` requirement on `Self` and `Self.Element` along with a new default constraint set that reinstates the requirements for existing code. Existing code would continue to apply all of the default sets, and doesn’t know about the new constraint set yet so would not suppress the newly lifted requirements: + +``` +protocol Container: ~Copyable, ~Escapable, + // added in v2: + ~Runcible { + associatedtype BorrowingIterator: BorrowingIteratorProtocol, + ~Copyable, ~Escapable + associatedtype Element: ~Copyable, ~Escapable, + // added in v2: + ~Runcible + + associatedtype SubContainer: Container /*implies where SubContainer: C,E,R*/ + + default constraintset Copying where Self: Copyable, Self.Element: Copyable + default constraintset Escaping where Self: Escapable, Self.Element: Escapable + // added in v2 to maintain compatibility: + default constraintset Runcing where Self: Runcible, Self.Element: Runcible +} + +// These all retain their meaning from v1: +extension Container {} +extension Container without Copying {} +extension Container without Escaping {} +extension Container without Copying, Escaping {} + +// In v2, code can now do the following for maximum permissivity: +extension Container without Copying, Escaping, Runcing {} +``` From 5207247487b88d5b600216336d7b40c0bf361cb5 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 8 Oct 2025 09:58:53 -0700 Subject: [PATCH 09/11] add acknowledgements --- proposals/NNNN-suppressed-associated-types.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 5b05bd54ef..ea02c62ef4 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -1,7 +1,7 @@ # Suppressed Default Conformances on Associated Types * Proposal: [SE-NNNN](NNNN-filename.md) -* Authors: [Kavon Farvardin](https://github.com/kavon), [Slava Pestov](https://github.com/slavapestov) +* Authors: [Kavon Farvardin](https://github.com/kavon) * Review Manager: TBD * Status: **Awaiting review** * Implementation: on `main` and available since at least Swift 6.1.2, using `-enable-experimental-feature SuppressedAssociatedTypes` @@ -366,3 +366,11 @@ extension Container without Copying, Escaping {} // In v2, code can now do the following for maximum permissivity: extension Container without Copying, Escaping, Runcing {} ``` + +## Acknowledgements + +I'd like to thank the following people for their discussion, insights and/or +contributions throughout the development of this proposal: + +- [Slava Pestov](https://github.com/slavapestov) +- [Joe Groff](https://github.com/jckarter) From c6a4d2b5e75c769eddea82745b3428847908ff83 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 8 Oct 2025 12:33:21 -0700 Subject: [PATCH 10/11] elaborate on the primary associated type idea --- proposals/NNNN-suppressed-associated-types.md | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index ea02c62ef4..fa6fd85c4f 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -123,7 +123,7 @@ extension Queue where Self: ~Copyable { However, with the current proposal, this defaulting behavior does not extend to associated types -with supressed conformances. In particular, no implicit +with suppressed conformances. In particular, no implicit `Self.Element: Copyable` requirement is introduced above, by either extension. Instead, a protocol extension for queue types where **both** the queue itself and the element @@ -194,6 +194,7 @@ struct QueueHolder: ~Copyable {} extension QueueHolder: Copyable where Q.Element: Copyable {} // error ``` This restriction is for runtime implementation reasons. + ## Source Compatibility @@ -268,9 +269,12 @@ Some possibilities for how this might look include: #### Defaulting only for primary associated types -Primary associated types have a strong correlation to what one might consider the core interface of a protocol. They also can’t be added to or removed once declared without breaking source compatibility. So we could only default primary associated types: +Primary associated types have a strong correlation to what one might consider +the core interface of a protocol. They also can’t be added to or removed once +declared without breaking source compatibility. +So we could only default primary associated types: -``` +```swift protocol Container { associatedtype Element: ~Copyable & ~Escapable associatedtype Iterator: ~Copyable & ~Escapable @@ -282,7 +286,23 @@ func foo() {...} ``` In order to avoid creating an infinite set of defaults, this would not be -recursive, but only apply to +recursive. One idea is to only apply it to the first level of associated types: + +```swift +protocol Sliceable: ~Copyable { + associatedtype Items: Sliceable & ~Copyable + consuming func split() -> (Self.Items, Self.Items) +} + +func flatten(_ root: S) { + // defaults to S: Copyable, S.Items: Copyable + // but leaves all other recursive associated types unconstrained, such as + // S.Item.Item and all those matching the regular expression S.Item[.Item]+ + + let one: S.Items = root.split().0 // 'one' is Copyable + let two: S.Items.Items = one.split().0 // 'two' is noncopyable +} +``` One drawback of this approach is that it would make adding a `~Copyable` and/or `~Escapable` associated type as a primary associated type to a protocol that From 8ad65112e4aba6e85bf227c1af14c80302758f93 Mon Sep 17 00:00:00 2001 From: John McCall Date: Tue, 16 Dec 2025 17:14:09 -0500 Subject: [PATCH 11/11] Fix pitch link, link to second pitch --- proposals/NNNN-suppressed-associated-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index fa6fd85c4f..49fe8da518 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -6,7 +6,7 @@ * Status: **Awaiting review** * Implementation: on `main` and available since at least Swift 6.1.2, using `-enable-experimental-feature SuppressedAssociatedTypes` * Previous Proposals: [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), [SE-446: Nonescapable Types](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0446-non-escapable.md) -* Review: [Pitch](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880) +* Review: ([first pitch](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880)) ([second pitch](https://forums.swift.org/t/pitch-suppressed-associated-types-with-defaults/83663)) ## Introduction