From 01cd3c7b3a62bd0d9498aeebb12f31e51273dc3d Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Fri, 6 Sep 2024 18:21:25 -0700 Subject: [PATCH 1/7] Draft 1 --- proposals/NNNN-suppressed-associated-types.md | 729 ++++++++++++++++++ 1 file changed, 729 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..e88a65593b --- /dev/null +++ b/proposals/NNNN-suppressed-associated-types.md @@ -0,0 +1,729 @@ +# Suppressed Default Conformances on Associated Types With Defaults + +* Proposal: [SE-NNNN](NNNN-filename.md) +* Authors: [Kavon Farvardin](https://github.com/kavon) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: on `main` using `-enable-experimental-feature SuppressedAssociatedTypesWithDefaults` +* 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: TBD + +## Introduction + +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: ~Copyable { + associatedtype Element + associatedtype Allocator = DefaultAllocator + + init() + init(alloc: Allocator) + + mutating func push(_: Self.Element) + mutating func pop() -> Self.Element +} +``` +While the conforming type is itself permitted to be noncopyable, its `Element` +type witness has to be `Copyable`: +```swift +/// error: LinkedList does not conform to Queue +struct LinkedList: ~Copyable, Queue { + ... +} +``` +This is an expressivity limitation in practice, and there is no workaround +possible today. + +## Proposed Solution + +The existing syntax for suppressing these default conformances is extended to +associated type declarations: + +```swift +/// Correct Queue protocol. +protocol Queue: ~Copyable { + associatedtype Element: ~Copyable + associatedtype Allocator: ~Copyable = DefaultAllocator + + init() + init(alloc: consuming Allocator) + + mutating func push(_: consuming Self.Element) + mutating func pop() -> Self.Element +} +``` + +Now, `LinkedList` can conform to `Queue`, as its witness for +`Queue.Element` is not required to be `Copyable`. +Similarly, stating `~Escapable` is allowed, to suppress the default +conformance requirement for `Escapable`. +Unless otherwise noted, any discussion of `~Copyable` types applies equivalently +to `~Escapable` types in this proposal. + +### Defaulting Behavior + +Swift's philosophy behind +[defaulting](0427-noncopyable-generics.md#default-conformances-and-suppression) +generic parameters to be `Copyable` (and `Escapable`) is rooted in the idea that +programmers expect their types to have that ability. +Library authors choosing to generalize their design with support for `~Copyable` +generics will not impose a burden of annotation on the common user, because +Swift will default their extensions and generic parameters to still be `Copyable`. +This idea serves as the foundation of the defaulting behavior for associated types. + +Here is a simplistic protocol for a `Buffer` that imposes no Copyable +requirements: + +```swift +protocol Buffer: ~Copyable { + associatedtype Data: ~Copyable + associatedtype Parser: ~Copyable + ... +} +``` + +Recall the existing rules from +[SE-427: Noncopyable Generics](0427-noncopyable-generics.md). Under +those rules, a protocol extension of `Buffer` always introduces a +default `Self: Copyable` requirement, since the protocol itself doesn't require it. + +By this proposal, default conformance requirements will also be introduced if any of a protocol's +_primary_ associated types (those appearing in angle brackets) are suppressed. +For `Buffer`, that means only a default `Data: Copyable` is introduced, +not one for the ordinary associated type `Parser`, when constraining the generic +parameter `B` to conform to `Buffer`: + +```swift +// by default, B: Copyable, B.Data: Copyable +func read(_ bytes: [B.Data], into: B) { ... } +``` + +Unlike a primary associated type, an ordinary associated type is not typically +used generically by conformers. +This rationale is in line with the original reason there is a distinction among +associated types from [SE-346](0346-light-weight-same-type-syntax.md): + +> Primary associated types are intended to be used for associated types which are usually provided by the caller. +> These associated types are often witnessed by generic parameters of the conforming type. + +The type `Buffer` is an example of this, as users often will build utilities +that deal with the `Data` generically, not `Parser`. Consider these example +conformers, + +```swift +struct BinaryParser: ~Copyable { ... } +struct TextParser { ... } + +class DecompressingReader: Buffer { + typealias Parser = BinaryParser +} + +struct Reader: Buffer { + typealias Parser = TextParser +} + +// by default, Self.Copyable, Self.Data: Copyable +extension Buffer { + // `valid` is provided for both DecompressingReader and Reader + func valid(_ bytes: [UInt8]) -> Bool { ... } +} +``` + +If ordinary associated types like `Buffer.Parser` were to default to +`Copyable`, then the extension of `Buffer` adding a `valid` method would exclude conformers +that witnessed the `Parser` with a noncopyable type, despite that being an +implementation detail. + +## 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. + +This proposal extends 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 Self.A: ~Copyable } +protocol P where Self.A: ~Copyable { associatedtype A } +``` + +### Expansion Procedure + +While building the [generic signature](https://download.swift.org/docs/assets/generics.pdf) +for a declaration, such as a generic function or type, the expansion procedure adds +infers extra requirements based on the desugared requirements of that declaration. +The procedure itself is simple, + +> Suppose there is a protocol `P` that declares primary associated types `A1, ..., An`. +> If there exists a desugared requirement `Subject: P`, the procedure infers +> the extra requirements `Subject.A1: IP, ..., Subject.An: IP`, for each invertible +> protocol `IP ∈ {Copyable, Escapable}`. +> If there exists a validly scoped inverse requirement `Subject.A1: ~IP`, then +> that cancels out the inferred requirement `Subject.A1: IP`, for any invertible +> protocol `IP`. + +As in [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), +after building the declaration's generic signature, if there was an inverse +requirement `Thing: ~IP` and yet `Thing` must conform to `IP` anyway, then the +inverse requirement is diagnosed as invalid. + +Default requirements become fixed once the generic signature is built for that +declaration, after applying the expansion procedure. For example, + +```swift +protocol Pushable { + associatedtype Element: ~Copyable +} + +struct Stack {} + +func push(_ s: Stack, _ v: Val) + where Val.Element: ~Copyable // error + {} +``` + +When the generic signature of `Stack` is built, the expansion procedure adds +the requirement `Scope.Element: Copyable`, because of the requirement +`Scope: Pushable` and `Element` being a primary associated type. The minimized +generic signature of `Stack` becomes, + +``` + +``` + +When building the generic signature of `push`, requirement inference consults +the generic signature of `Stack`, adding the hard requirements that +`Val == Scope` and `Scope.Element: Copyable`. There is no way to satisfy the +inverse requirement `Val.Element: ~Copyable` without mutating the generic +signature of `Stack`, which is not permitted, so the inverse requirement is +illegally scoped in `push`. + +Inverses can apply across equality constraints _within the same declaration's generic signature_ +to cancel-out default requirements from primary associated types. Consider this example, + +```swift +protocol Iterable: ~Copyable { + associatedtype Element: ~Copyable +} + +struct Cursor: Iterable where Value: ~Copyable {} +``` + +In `Cursor`, the inheritance clause contains `Iterable` yielding the +desugared requirements `Element: Copyable` and `Element == Value`. +There is also an inverse requirement `Value: ~Copyable` within `Cursor` +that cancels-out the default requirement `Element: Copyable` inferred from +the conformance to `Iterable`. Thus, the inverse requirement is well-scoped. + +### Protocol inheritance + +Defaulting 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 either of the following are true: + + 1. it is a primary associated type in the base protocol + 2. the derived protocol re-states the associated type + +Here are some examples, +```swift +protocol Base { + associatedtype A: ~Copyable + associatedtype B: ~Copyable +} + +// Case 1: 'A' requires Copyable because it's a primary associated type in Base +protocol Derived1: Base { + // 'A' is Copyable + // 'B' is ~Copyable +} + +// Case 2, Restating the associated type B infers fresh defaults for it. +protocol Derived2: Base { + // 'A' is Copyable + // 'B' is Copyable + associatedtype B +} +``` + +It is possible to suppress the default coming from a base protocol via Case 1 +using an inverse requirement: + +```swift +// Derived3 suppresses the default from Case 1. +protocol Derived3: Base where Self.A: ~Copyable { + // 'A' is ~Copyable + // 'B' is ~Copyable +} +``` + +Elevating an ordinary associated type from a base protocol to a primary +associated type (such as in `Child`) will not infer a default within that +particular protocol, + +```swift +protocol Child: Base { + // 'A' is Copyable + // 'B' is ~Copyable +} +``` + +But that elevation it will infer defaults in protocols downstream of it, +such as in `Grandchild`: + +```swift +// Case 1: Child.B is a primary associated type +protocol Grandchild: Child { + // 'A' is Copyable + // 'B' is Copyable +} +``` + +It is illegal to suppress the `Copyable` requirements +on `A` or `B` in a protocol derived from `Grandchild`, as they become fixed +in the `Grandchild` protocol: + +```swift +protocol GrandGrandchild: Grandchild { + // 'A' is Copyable + // 'B' is Copyable +} +``` + +Case 1 does not apply in `GrandGrandchild` to permit suppression +on 'A' or 'B' coming from `Grandchild`. It is the same situation as this: + +```swift +protocol Bird { + associatedtype Song +} + +protocol Eagle: Bird where Self.Song: ~Copyable {} +``` + +where `Bird` declares a `Song` that is an associated type with a fixed `Copyable` +requirement; it not suppressed and not defaulted in `Eagle` via Case 1. + +### Extensions + +Consider this simple protocol for iteration, + +```swift +protocol Iterable: ~Copyable { + associatedtype Element: ~Copyable + ... +} +``` + +An extension of `Iterable` introduces a default for `Element` because it is +a primary associated type, which is suppressible: + +```swift +// implicitly, where Self: Copyable, Self.Element: Copyable +extension Iterable {} + +// implicitly, where Self: Copyable +extension Iterable where Element: ~Copyable {} + +// fully without defaults +extension Iterable where Self: ~Copyable, Element: ~Copyable {} +``` + +For ordinary associated types like `Strategy` in the next example, +no default is inferred, + +```swift +protocol PersistedDictionary: ~Copyable { + associatedtype Key: ~Copyable + associatedtype Value: ~Copyable + associatedtype Strategy: ~Copyable +} + +// implicitly, where Self: Copyable, Self.Key: Copyable, Self.Value: Copyable +extension PersistedDictionary {} +``` + +An inverse requirement in an extension of a protocol that conflicts with the +protocol's requirement signature is invalid, + +```swift +protocol Viewable: Iterable {} + +extension Viewable where Element: ~Copyable {} +// ^ error: 'Self.Element' required to be 'Copyable' but is marked with '~Copyable' +``` + +`Viewable`'s requirement signature includes `Self.Element: Copyable` from +its inheritance of `Iterable`, which has its `Element` as a primary associated +type. Once that becomes fixed in `Viewable`'s signature, extensions of it cannot +remove the `Copyable` requirement. The inverse requirement must be stated on +`Viewable` itself to suppress the default requirement from `Iterable`. + +### Existentials + +Suppose we have the protocol, + +```swift +protocol Source: ~Copyable { + associatedtype Element: ~Copyable + associatedtype Generator: ~Copyable + + func element() -> Element + func generator() -> Generator +} +``` + +Existentials such as `any P` work similarly to a generic signature reified as +a value, where the single generic parameter `T` has a conformance requirement +`T: P`, + +```swift +func ex1(_ s: any Source) { + let e = s.element() // <- Copyable + let g = s.generator() // <- NOT Copyable +} +``` + +> TODO: the following is not yet implemented! + +It is possible to suppress the default requirement on an associated type using +a new syntax `any Protocol`: + +```swift +func ex1(_ s: any Source) { + let e = s.element() // <- NOT Copyable + let g = s.generator() // <- NOT Copyable +} +``` + +### Recursion + +Protocol requirements can be such that an infinite number of associated types +are derivable from a type conforming to that protocol. A classic example is, + +```swift +protocol P: ~Copyable { + associatedtype A: ~Copyable, P +} +``` + +For a generic environment ``, all of `R.A, R.A.A, R.A.A.A, ...`, +are Copyable because the type with one fewer `A` conforms to `P`. + +Next, consider this pair of mutually recursive protocols where only one of +them has a primary associated type, + +```swift +protocol First: ~Copyable { + associatedtype A: ~Copyable, Second +} + +protocol Second: ~Copyable { + associatedtype B: ~Copyable, First +} +``` + +For a generic environment ``, we observe that any archetype +rooted in `T` and ending with an `A`, such `T.A.B.A`, is Copyable because +`T.A.B: First` and `First` has a primary associated type `A`. +Similarly, for an archetype that ends in a `B`, it is not Copyable, because +`T.*.A: Second` which has only the ordinary associated type `B`. So we get an +alternating pattern for the defaults, + +``` +T.A : Copyable +T.B : ~Copyable +T.B.A: Copyable +T.B.A.B: ~Copyable +T.B.A.B.A: Copyable +... +``` + +The proof by induction is left as an exercise for the reader. + + +### Default Witnesses + +An associated type can already declare a default _witness_, which is a type that +is used to witness an associated type requirement, if the conforming type does +not specify one. For example, in `SegmentedArray`'s conformance to `Queue`, it +doesn't declare a nested type named `Allocator` satisfying the requirements of +`Queue.Allocator`, so it automatically uses `DefaultAllocator`: +```swift +protocol Queue: ~Copyable { + associatedtype Element: ~Copyable + associatedtype Allocator: Alloc & ~Copyable = DefaultAllocator + ... + `init(alloc: consuming Allocator)` +} +protocol Alloc: ~Copyable { ... } +struct DefaultAllocator: Alloc { ... } +``` +The `DefaultAllocator` conforms to `Copyable`, which is simple for conformers +when implementing the rest of `Queue`'s requirements: +```swift +struct SegmentedArray: Queue { + // uses DefaultAllocator by default + init(alloc: DefaultAllocator) +} +``` +By the defaulting rules in this proposal, a generic type parameter `Q` +constrained to `Queue` will *not* assume `Q.Allocator` is Copyable, since it is +an ordinary associated type: +```swift +// by default, Q.Element: Copyable +func createSubQueues(_ kind: Q.Type, + n: Int, + with alloc: borrowing Q.Allocator) -> [Q] { + // Q.Allocator is ~Copyable +} +``` +Thus, even if the default witness for a suppressed associated type conforms to +`Copyable` and/or `Escapable`, matching generic requirement(s) *are not* +introduced. + +> **Rationale:** Part of the reason for this is understandability, as it's +> possible for the default witness to have a conditional conformance for +> `Copyable` or `Escapable`. For example, +> +> ```swift +> struct ForwardIterator: ~Copyable { ... } +> extension ForwardIterator: Copyable where Item: Copyable { ... } +> +> protocol Iterable { +> associatedtype Element: ~Copyable +> associatedtype Iter: ~Copyable = ForwardIterator +> +> func getIter() -> Iter +> } +> ``` +> The default witness for `Iterable.Iter` is `ForwardIterator`, which is only +> `Copyable` if the `Element` is `Copyable`. Thus, the default constraints for +> `Iter` would vary depending on the `Element` type in these functions: +> +> ```swift +> func runForwardsInt(_ it: some Iterable) { +> _ = copy it.getIter() // OK +> } +> +> func runForwardsNC(_ it: some Iterable) { +> _ = copy it.getIter() // error: copy of noncopyable type +> } +> ``` +> The same goes for extensions of the protocol. + + +### Library evolution and new associated type requirements + +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. + +Suppose a new primary associated type is introduced that is `~Copyable` and +the default witness does not conform to `Copyable`: + +```swift +protocol Foo { + // Added in v2 + associatedtype New: ~Copyable +} + +struct NC: ~Copyable {} + +// Added in v2 +extension Foo where New: ~Copyable { typealias New = NC } +``` + +Because of the defaulting behavior of primary associated type to `Copyable`, +and the choice of providing a non-`Copyable` default witness, this can change +the meaning of source code when it compiles against the new definition of the +protocol: + +```swift +struct ExistingConformance: Foo {} + +// `T: Foo` implies `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()) +} +``` + +Thus, the default witness must be carefully chosen to avoid a source break. + +### Conditional conformance + +Finally, recall that concrete types may conform to `Copyable` and +`Escapable` conditionally, depending on the copyability or +escapability of a generic parameter. Even though associated types +may now suppress conformance to these protocols, a conditional +conformance to `Copyable` or `Escapable` that depends on an +associated type is still not allowed: +```swift +struct QueueHolder: ~Copyable {} +extension QueueHolder: Copyable where Q.Element: Copyable {} // error +``` +This restriction is for runtime implementation reasons. + + + + +## 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. + +One of the goals of this proposal is to make it safe to suppress a conformance on an +*existing* primary associated type. A protocol's set of primary associated types +can’t be added to or removed once declared without breaking source compatibility. + +Changing an existing *ordinary* associated type declaration to suppress +conformance to `Copyable` or `Escapable` is also a **source-breaking** change. +For example, if a library publishes this protocol: +```swift +public protocol Manager: ~Copyable { + associatedtype Resource +} +``` +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 +extension Manager where Self: ~Copyable { + func makeCopies(_ r: Self.Resource) -> (Self.Resource, Self.Resource) { + return (r, r) + } +} +``` +Now suppose the library author then changes the protocol to +suppress conformance: +```swift +public protocol Manager: ~Copyable { + associatedtype Resource: ~Copyable +} +``` +The client's extension of `Manager` will no longer type check, because +the body of `makeCopies()` assumes `r` is `Copyable`, and this +assumption is no longer true. + +## ABI Compatibility + +The ABI of existing code is not affected by this proposal. + +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. + +## Alternatives Considered + +### No recursion + +A prior version of this proposal [was pitched](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880) that was absent of any defaulting behavior +for associated types. + +### Definition-driven associated type defaults + +Rather than try to impose a blanket default on all primary 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 come at the expense of +increased language complexity. Readers would have to carefully consult the +definitions of protocols to see whether they come with default `Copyable` +or `Escapable` requirements on their associated types. + +Some possibilities for how this might look include: + +#### 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. + +```swift +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, *named* sets of constraints, which can be individually suppressed as a group. For instance, this would make it possible to provide configurations of a protocol to suppress copying and escaping individually, without making developers write out the entire set of constraint suppressions: + +```swift +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 the '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: + +```swift +protocol Container: ~Copyable, ~Escapable, ~Runcible { + // ^ added in v2 + + associatedtype BorrowingIterator: BorrowingIteratorProtocol, + ~Copyable, ~Escapable + associatedtype Element: ~Copyable, ~Escapable, ~Runcible + // ^ added in v2: + + + 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 {} +``` + +## 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 5e51bd8b3cc9af7cb16b2c47fa4233014b34d568 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 10 Dec 2025 08:15:26 -0800 Subject: [PATCH 2/7] avoid "type witness" in introduction --- proposals/NNNN-suppressed-associated-types.md | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index e88a65593b..79a9fb137b 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -10,9 +10,9 @@ ## Introduction -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: +An associated type defines a generic type in a protocol. +You use them to help define the protocol's requirements. +This Queue has two associated types, Element and Allocator: ```swift /// Queue has no reason to require Element to be Copyable. protocol Queue: ~Copyable { @@ -22,20 +22,31 @@ protocol Queue: ~Copyable { init() init(alloc: Allocator) - mutating func push(_: Self.Element) - mutating func pop() -> Self.Element + mutating func push(_: Element) + mutating func pop() -> Element + // ... } ``` +The first associated type Element represents the type of value that by which +`push` and `pop` must be defined. + +Any type conforming to Queue must define a nested type Element that satisfies +the protocol's requirements, of which there are no _explicit_ requirements. While the conforming type is itself permitted to be noncopyable, its `Element` -type witness has to be `Copyable`: +type has to be `Copyable`: ```swift /// error: LinkedList does not conform to Queue +/// note: Element is required to be Copyable struct LinkedList: ~Copyable, Queue { ... } ``` -This is an expressivity limitation in practice, and there is no workaround -possible today. +This is because in [SE-427: Noncopyable Generics](0427-noncopyable-generics.md), +an implicit requirement that `Queue.Element: Copyable & Escapable` is inferred, +with no way to suppress it in that protocol. +This is expressivity limitation in practice, as it prevents Swift programmers +from defining protocols that work with noncopyable or nonescapable associated +types. ## Proposed Solution @@ -56,8 +67,8 @@ protocol Queue: ~Copyable { } ``` -Now, `LinkedList` can conform to `Queue`, as its witness for -`Queue.Element` is not required to be `Copyable`. +Now, `LinkedList` can conform to `Queue`, as its `Element` is not required to be +`Copyable`. Similarly, stating `~Escapable` is allowed, to suppress the default conformance requirement for `Escapable`. Unless otherwise noted, any discussion of `~Copyable` types applies equivalently From b1a56378774405cb2c909738c017984951930c69 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 10 Dec 2025 12:00:12 -0800 Subject: [PATCH 3/7] introduce "witness" in parenthetical --- proposals/NNNN-suppressed-associated-types.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 79a9fb137b..973b9f6e4d 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -30,8 +30,10 @@ protocol Queue: ~Copyable { The first associated type Element represents the type of value that by which `push` and `pop` must be defined. -Any type conforming to Queue must define a nested type Element that satisfies -the protocol's requirements, of which there are no _explicit_ requirements. +Any type conforming to Queue must define a nested type Element that satisfies +(or _witnesses_) the protocol's requirements for its Element. +This nested type could be a generic parameter named Element, a typealias named +Element, and so on. While the conforming type is itself permitted to be noncopyable, its `Element` type has to be `Copyable`: ```swift From 4c8612d12cdd6caa63fd1c2a1dc3e0262bce5914 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 10 Dec 2025 12:01:26 -0800 Subject: [PATCH 4/7] clarify existentials and do not propose `any P` --- proposals/NNNN-suppressed-associated-types.md | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 973b9f6e4d..69273b8232 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -393,9 +393,9 @@ protocol Source: ~Copyable { } ``` -Existentials such as `any P` work similarly to a generic signature reified as -a value, where the single generic parameter `T` has a conformance requirement -`T: P`, +Existentials such as `any P` work similarly to the case of a single generic +parameter `T` has a conformance requirement `T: P`. The expansion of +defaults happens here as well, ```swift func ex1(_ s: any Source) { @@ -404,13 +404,12 @@ func ex1(_ s: any Source) { } ``` -> TODO: the following is not yet implemented! - -It is possible to suppress the default requirement on an associated type using -a new syntax `any Protocol`: +It's possible to constrain the existential using a generic type parameter, +which will suppress the defaults expansion for the primary associated type of +`Source`, ```swift -func ex1(_ s: any Source) { +func ex2(_ s: any Source) { let e = s.element() // <- NOT Copyable let g = s.generator() // <- NOT Copyable } @@ -639,6 +638,26 @@ 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. +## Future Directions + +It's possible to imagine additional functionality that could one day be +supported, but is not part of this proposal. + +### Constrained Existentials via `some` + +There is some support for constrained existentials, such as + +```swift +func f(_ e: any P) {} +``` + +It might be a generally useful feature if there were support for a syntax such as +`any P` to permit the constrained existential to carry with it the +constraint that its primary associated type conforms to Hashable. +That syntax could then be extended to allow suppression of defaults in the +constrained existential via `any Q`. + + ## Alternatives Considered ### No recursion From 6dcf76c7c8f464a1a514d8422a271e04eb1a285a Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Wed, 10 Dec 2025 15:09:03 -0800 Subject: [PATCH 5/7] Draft 2 --- proposals/NNNN-suppressed-associated-types.md | 307 ++++++++++++------ 1 file changed, 203 insertions(+), 104 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index 69273b8232..2c88e7581e 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -8,6 +8,33 @@ * 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: TBD +**Table of Contents** +- [Suppressed Default Conformances on Associated Types With Defaults](#suppressed-default-conformances-on-associated-types-with-defaults) + - [Introduction](#introduction) + - [Proposed Solution](#proposed-solution) + - [Defaulting Behavior](#defaulting-behavior) + - [Detailed Design](#detailed-design) + - [Expansion Procedure](#expansion-procedure) + - [Limits of Suppression](#limits-of-suppression) + - [Protocol inheritance](#protocol-inheritance) + - [Extensions](#extensions) + - [Existentials](#existentials) + - [Recursion](#recursion) + - [Default Witnesses](#default-witnesses) + - [Library evolution and new associated type requirements](#library-evolution-and-new-associated-type-requirements) + - [Conditional conformance](#conditional-conformance) + - [Source Compatibility](#source-compatibility) + - [ABI Compatibility](#abi-compatibility) + - [Future Directions](#future-directions) + - [Constrained Existentials via `some`](#constrained-existentials-via-some) + - [Alternatives Considered](#alternatives-considered) + - [No recursion](#no-recursion) + - [Definition-driven associated type defaults](#definition-driven-associated-type-defaults) + - [Protocol-defined default requirements](#protocol-defined-default-requirements) + - [Default constraint sets](#default-constraint-sets) + - [Acknowledgements](#acknowledgements) + + ## Introduction An associated type defines a generic type in a protocol. @@ -27,15 +54,15 @@ protocol Queue: ~Copyable { // ... } ``` -The first associated type Element represents the type of value that by which +The first associated type Element represents the type of value by which `push` and `pop` must be defined. Any type conforming to Queue must define a nested type Element that satisfies (or _witnesses_) the protocol's requirements for its Element. This nested type could be a generic parameter named Element, a typealias named Element, and so on. -While the conforming type is itself permitted to be noncopyable, its `Element` -type has to be `Copyable`: +While the type conforming to Queue is permitted to be noncopyable, its Element +type has to be Copyable: ```swift /// error: LinkedList does not conform to Queue /// note: Element is required to be Copyable @@ -44,10 +71,10 @@ struct LinkedList: ~Copyable, Queue { } ``` This is because in [SE-427: Noncopyable Generics](0427-noncopyable-generics.md), -an implicit requirement that `Queue.Element: Copyable & Escapable` is inferred, -with no way to suppress it in that protocol. -This is expressivity limitation in practice, as it prevents Swift programmers -from defining protocols that work with noncopyable or nonescapable associated +an implicit requirement that Queue.Element is both Copyable and Escapable is inferred, +with no way to suppress it. +This is an expressivity limitation in practice, as it prevents Swift programmers +from defining protocols in terms of noncopyable or nonescapable associated types. ## Proposed Solution @@ -69,10 +96,13 @@ protocol Queue: ~Copyable { } ``` -Now, `LinkedList` can conform to `Queue`, as its `Element` is not required to be -`Copyable`. +Now, LinkedList can conform to Queue, as its Element is not required to be +Copyable. +The associated type Allocator is also not required to be Copyable, meaning the +DefaultAllocator, which is used when the conformer doesn't define its own +Allocator, can be either Copyable or not. Similarly, stating `~Escapable` is allowed, to suppress the default -conformance requirement for `Escapable`. +conformance requirement for Escapable. Unless otherwise noted, any discussion of `~Copyable` types applies equivalently to `~Escapable` types in this proposal. @@ -80,14 +110,14 @@ to `~Escapable` types in this proposal. Swift's philosophy behind [defaulting](0427-noncopyable-generics.md#default-conformances-and-suppression) -generic parameters to be `Copyable` (and `Escapable`) is rooted in the idea that +generic parameters to be Copyable (and Escapable) is rooted in the idea that programmers expect their types to have that ability. Library authors choosing to generalize their design with support for `~Copyable` generics will not impose a burden of annotation on the common user, because -Swift will default their extensions and generic parameters to still be `Copyable`. -This idea serves as the foundation of the defaulting behavior for associated types. +Swift will default their extensions and generic parameters to still be Copyable. +This idea serves as the foundation of the proposed defaulting behavior for associated types. -Here is a simplistic protocol for a `Buffer` that imposes no Copyable +Here is a simplistic protocol for a Buffer that imposes no Copyable requirements: ```swift @@ -100,14 +130,13 @@ protocol Buffer: ~Copyable { Recall the existing rules from [SE-427: Noncopyable Generics](0427-noncopyable-generics.md). Under -those rules, a protocol extension of `Buffer` always introduces a +those rules, a protocol extension of Buffer always introduces a default `Self: Copyable` requirement, since the protocol itself doesn't require it. By this proposal, default conformance requirements will also be introduced if any of a protocol's _primary_ associated types (those appearing in angle brackets) are suppressed. -For `Buffer`, that means only a default `Data: Copyable` is introduced, -not one for the ordinary associated type `Parser`, when constraining the generic -parameter `B` to conform to `Buffer`: +For Buffer, that means only a default `Data: Copyable` is introduced, +not one for the ordinary (non-primary) associated type Parser, when constraining the generic parameter `B` to conform to Buffer: ```swift // by default, B: Copyable, B.Data: Copyable @@ -122,8 +151,8 @@ associated types from [SE-346](0346-light-weight-same-type-syntax.md): > Primary associated types are intended to be used for associated types which are usually provided by the caller. > These associated types are often witnessed by generic parameters of the conforming type. -The type `Buffer` is an example of this, as users often will build utilities -that deal with the `Data` generically, not `Parser`. Consider these example +The type Buffer is an example of this, as users often will build utilities +that deal with the Data generically, not Parser. Consider these example conformers, ```swift @@ -146,8 +175,8 @@ extension Buffer { ``` If ordinary associated types like `Buffer.Parser` were to default to -`Copyable`, then the extension of `Buffer` adding a `valid` method would exclude conformers -that witnessed the `Parser` with a noncopyable type, despite that being an +Copyable, then the extension of Buffer adding a `valid` method would exclude conformers +that witnessed the Parser with a noncopyable type, despite that being an implementation detail. ## Detailed Design @@ -159,7 +188,7 @@ There are three ways to impose a requirement on an associated type: This proposal extends 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 +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 } @@ -180,13 +209,15 @@ The procedure itself is simple, > protocol `IP ∈ {Copyable, Escapable}`. > If there exists a validly scoped inverse requirement `Subject.A1: ~IP`, then > that cancels out the inferred requirement `Subject.A1: IP`, for any invertible -> protocol `IP`. +> protocol IP. As in [SE-427: Noncopyable Generics](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md), after building the declaration's generic signature, if there was an inverse -requirement `Thing: ~IP` and yet `Thing` must conform to `IP` anyway, then the +requirement `Thing: ~IP` and yet Thing must conform to IP anyway, then the inverse requirement is diagnosed as invalid. +### Limits of Suppression + Default requirements become fixed once the generic signature is built for that declaration, after applying the expansion procedure. For example, @@ -202,22 +233,59 @@ func push(_ s: Stack, _ v: Val) {} ``` -When the generic signature of `Stack` is built, the expansion procedure adds +When the generic signature of Stack is built, the expansion procedure adds the requirement `Scope.Element: Copyable`, because of the requirement -`Scope: Pushable` and `Element` being a primary associated type. The minimized -generic signature of `Stack` becomes, +`Scope: Pushable` and Element being a primary associated type. The minimized +generic signature of Stack becomes, ``` ``` When building the generic signature of `push`, requirement inference consults -the generic signature of `Stack`, adding the hard requirements that +the generic signature of Stack, adding the hard requirements that `Val == Scope` and `Scope.Element: Copyable`. There is no way to satisfy the inverse requirement `Val.Element: ~Copyable` without mutating the generic -signature of `Stack`, which is not permitted, so the inverse requirement is +signature of Stack, which is not permitted, so the inverse requirement is illegally scoped in `push`. +The same concept applies to the requirement signatures of a protocol becoming +fixed after expansion is applied to it locally. Consider this protocol `P`, + +```swift +protocol P: ~Copyable { + associatedtype A: ~Copyable +} +``` + +Its requirement signature is ``, +because the default Copyable requirements were suppressed on both Self and +`Self.A`. Next, consider this protocol `Q`, + +```swift +protocol Q: ~Copyable { + associatedtype B: ~Copyable, P +} +``` + +The expansion procedure applies locally to `Q` as follows. +The desugared requirement `Self.B: P` implies `Self.B.A: Copyable`, yielding the now-fixed requirement signature: + +``` + + ~~~~~~~~~~~~~~~~~~~ + from expansion procedure +``` + +Constraining a generic type parameter `T` to conform to `Q` only permits suppression of a default inferred for `T.B`, not `T.B.A`: + +```swift +func limits(_ t: T) + where T.B: ~Copyable, + T.B.A: ~Copyable // error: T.B.A is required to be Copyable + {} +``` + Inverses can apply across equality constraints _within the same declaration's generic signature_ to cancel-out default requirements from primary associated types. Consider this example, @@ -229,11 +297,11 @@ protocol Iterable: ~Copyable { struct Cursor: Iterable where Value: ~Copyable {} ``` -In `Cursor`, the inheritance clause contains `Iterable` yielding the +In Cursor, the inheritance clause contains `Iterable` yielding the desugared requirements `Element: Copyable` and `Element == Value`. -There is also an inverse requirement `Value: ~Copyable` within `Cursor` +There is also an inverse requirement `Value: ~Copyable` within Cursor that cancels-out the default requirement `Element: Copyable` inferred from -the conformance to `Iterable`. Thus, the inverse requirement is well-scoped. +the conformance to Iterable. Thus, the inverse requirement is well-scoped. ### Protocol inheritance @@ -278,7 +346,7 @@ protocol Derived3: Base where Self.A: ~Copyable { ``` Elevating an ordinary associated type from a base protocol to a primary -associated type (such as in `Child`) will not infer a default within that +associated type (such as in Child) will not infer a default within that particular protocol, ```swift @@ -289,7 +357,7 @@ protocol Child: Base { ``` But that elevation it will infer defaults in protocols downstream of it, -such as in `Grandchild`: +such as in Grandchild: ```swift // Case 1: Child.B is a primary associated type @@ -299,9 +367,9 @@ protocol Grandchild: Child { } ``` -It is illegal to suppress the `Copyable` requirements -on `A` or `B` in a protocol derived from `Grandchild`, as they become fixed -in the `Grandchild` protocol: +It is illegal to suppress the Copyable requirements +on `A` or `B` in a protocol derived from Grandchild, as they become fixed +in the Grandchild protocol: ```swift protocol GrandGrandchild: Grandchild { @@ -310,8 +378,8 @@ protocol GrandGrandchild: Grandchild { } ``` -Case 1 does not apply in `GrandGrandchild` to permit suppression -on 'A' or 'B' coming from `Grandchild`. It is the same situation as this: +Case 1 does not apply in GrandGrandchild to permit suppression +on 'A' or 'B' coming from Grandchild. It is the same situation as this: ```swift protocol Bird { @@ -321,8 +389,8 @@ protocol Bird { protocol Eagle: Bird where Self.Song: ~Copyable {} ``` -where `Bird` declares a `Song` that is an associated type with a fixed `Copyable` -requirement; it not suppressed and not defaulted in `Eagle` via Case 1. +where Bird declares a Song that is an associated type with a fixed Copyable +requirement; it not suppressed and not defaulted in Eagle via Case 1. ### Extensions @@ -335,7 +403,7 @@ protocol Iterable: ~Copyable { } ``` -An extension of `Iterable` introduces a default for `Element` because it is +An extension of Iterable introduces a default for Element because it is a primary associated type, which is suppressible: ```swift @@ -349,7 +417,7 @@ extension Iterable where Element: ~Copyable {} extension Iterable where Self: ~Copyable, Element: ~Copyable {} ``` -For ordinary associated types like `Strategy` in the next example, +For ordinary associated types like Strategy in the next example, no default is inferred, ```swift @@ -373,11 +441,11 @@ extension Viewable where Element: ~Copyable {} // ^ error: 'Self.Element' required to be 'Copyable' but is marked with '~Copyable' ``` -`Viewable`'s requirement signature includes `Self.Element: Copyable` from -its inheritance of `Iterable`, which has its `Element` as a primary associated -type. Once that becomes fixed in `Viewable`'s signature, extensions of it cannot -remove the `Copyable` requirement. The inverse requirement must be stated on -`Viewable` itself to suppress the default requirement from `Iterable`. +Viewable's requirement signature includes `Self.Element: Copyable` from +its inheritance of Iterable, which has its Element as a primary associated +type. Once that becomes fixed in Viewable's signature, extensions of it cannot +remove the Copyable requirement. The inverse requirement must be stated on +Viewable itself to suppress the default requirement from Iterable. ### Existentials @@ -406,7 +474,7 @@ func ex1(_ s: any Source) { It's possible to constrain the existential using a generic type parameter, which will suppress the defaults expansion for the primary associated type of -`Source`, +Source, ```swift func ex2(_ s: any Source) { @@ -417,8 +485,8 @@ func ex2(_ s: any Source) { ### Recursion -Protocol requirements can be such that an infinite number of associated types -are derivable from a type conforming to that protocol. A classic example is, +There can be an infinite number of type parameters derivable from a conformance +requirement, because a protocol's associated type requirement can be part of a cycle with the protocol itself: ```swift protocol P: ~Copyable { @@ -426,8 +494,11 @@ protocol P: ~Copyable { } ``` -For a generic environment ``, all of `R.A, R.A.A, R.A.A.A, ...`, -are Copyable because the type with one fewer `A` conforms to `P`. +For a generic signature ``, all of the type parameters +`R.A, R.A.A, R.A.A.A, ...`, +are Copyable. +For any type parameter `X` rooted in `R`, the type `X.A` conforms to `P`, and by the expansion procedure, that implies `X.A: Copyable` because `A` is a primary +associated type of `P`. Next, consider this pair of mutually recursive protocols where only one of them has a primary associated type, @@ -442,32 +513,28 @@ protocol Second: ~Copyable { } ``` -For a generic environment ``, we observe that any archetype -rooted in `T` and ending with an `A`, such `T.A.B.A`, is Copyable because -`T.A.B: First` and `First` has a primary associated type `A`. -Similarly, for an archetype that ends in a `B`, it is not Copyable, because -`T.*.A: Second` which has only the ordinary associated type `B`. So we get an -alternating pattern for the defaults, +For a generic signature ``, we observe that any type parameter +rooted in T and ending with an A, such T.A.B.A, is Copyable because +`T.A.B: First` and First has a primary associated type A. +Similarly, for type parameter T.A.B which ends in a B, it is *not* required to conform to Copyable, because `T.A: Second` and Second has only +the ordinary associated type B. So there is an alternating pattern for the +defaults, ``` T.A : Copyable -T.B : ~Copyable -T.B.A: Copyable -T.B.A.B: ~Copyable -T.B.A.B.A: Copyable +T.A.B: ~Copyable +T.A.B.A: Copyable +T.A.B.A.B: ~Copyable ... ``` -The proof by induction is left as an exercise for the reader. - - ### Default Witnesses An associated type can already declare a default _witness_, which is a type that is used to witness an associated type requirement, if the conforming type does -not specify one. For example, in `SegmentedArray`'s conformance to `Queue`, it -doesn't declare a nested type named `Allocator` satisfying the requirements of -`Queue.Allocator`, so it automatically uses `DefaultAllocator`: +not specify one. For example, in SegmentedArray's conformance to Queue, it +doesn't declare a nested type named Allocator satisfying the requirements of +`Queue.Allocator`, so it automatically uses DefaultAllocator: ```swift protocol Queue: ~Copyable { associatedtype Element: ~Copyable @@ -478,8 +545,8 @@ protocol Queue: ~Copyable { protocol Alloc: ~Copyable { ... } struct DefaultAllocator: Alloc { ... } ``` -The `DefaultAllocator` conforms to `Copyable`, which is simple for conformers -when implementing the rest of `Queue`'s requirements: +The DefaultAllocator conforms to Copyable, which is simple for conformers +when implementing the rest of Queue's requirements: ```swift struct SegmentedArray: Queue { // uses DefaultAllocator by default @@ -487,7 +554,7 @@ struct SegmentedArray: Queue { } ``` By the defaulting rules in this proposal, a generic type parameter `Q` -constrained to `Queue` will *not* assume `Q.Allocator` is Copyable, since it is +constrained to Queue will *not* assume `Q.Allocator` is Copyable, since it is an ordinary associated type: ```swift // by default, Q.Element: Copyable @@ -498,12 +565,12 @@ func createSubQueues(_ kind: Q.Type, } ``` Thus, even if the default witness for a suppressed associated type conforms to -`Copyable` and/or `Escapable`, matching generic requirement(s) *are not* +Copyable and/or Escapable, matching generic requirement(s) *are not* introduced. > **Rationale:** Part of the reason for this is understandability, as it's > possible for the default witness to have a conditional conformance for -> `Copyable` or `Escapable`. For example, +> Copyable or Escapable. For example, > > ```swift > struct ForwardIterator: ~Copyable { ... } @@ -516,9 +583,9 @@ introduced. > func getIter() -> Iter > } > ``` -> The default witness for `Iterable.Iter` is `ForwardIterator`, which is only -> `Copyable` if the `Element` is `Copyable`. Thus, the default constraints for -> `Iter` would vary depending on the `Element` type in these functions: +> The default witness for `Iterable.Iter` is ForwardIterator, which is only +> Copyable if the Element is Copyable. Thus, the default constraints for +> Iter would vary depending on the Element type in these functions: > > ```swift > func runForwardsInt(_ it: some Iterable) { @@ -539,7 +606,7 @@ requirements, without breaking source or binary compatibility, as long as a default implementation is provided for existing code. Suppose a new primary associated type is introduced that is `~Copyable` and -the default witness does not conform to `Copyable`: +the default witness does not conform to Copyable: ```swift protocol Foo { @@ -553,8 +620,8 @@ struct NC: ~Copyable {} extension Foo where New: ~Copyable { typealias New = NC } ``` -Because of the defaulting behavior of primary associated type to `Copyable`, -and the choice of providing a non-`Copyable` default witness, this can change +Because of the defaulting behavior of primary associated type to Copyable, +and the choice of providing a noncopyable default witness, this can change the meaning of source code when it compiles against the new definition of the protocol: @@ -566,7 +633,7 @@ 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 + // ExistingConformance.New defaults to noncopyable type NC, so doesn't // satisfy the default `T.New: Copyable` requirement. existingFunction(ExistingConformance()) } @@ -576,19 +643,20 @@ Thus, the default witness must be carefully chosen to avoid a source break. ### Conditional conformance -Finally, recall that concrete types may conform to `Copyable` and -`Escapable` conditionally, depending on the copyability or +Finally, recall that concrete types may conform to Copyable and +Escapable conditionally, depending on the copyability or escapability of a generic parameter. Even though associated types may now suppress conformance to these protocols, a conditional -conformance to `Copyable` or `Escapable` that depends on an +conformance to Copyable or Escapable that depends on an associated type is still not allowed: ```swift -struct QueueHolder: ~Copyable {} -extension QueueHolder: Copyable where Q.Element: Copyable {} // error +protocol Goose: ~Copyable { associatedtype Quack: ~Copyable } +struct Pond: ~Copyable {} +extension QueueHolder: Copyable where G.Quack: Copyable {} // error ``` -This restriction is for runtime implementation reasons. +This restriction is for runtime implementation limitations. - + ## Source Compatibility @@ -602,7 +670,7 @@ One of the goals of this proposal is to make it safe to suppress a conformance o can’t be added to or removed once declared without breaking source compatibility. Changing an existing *ordinary* associated type declaration to suppress -conformance to `Copyable` or `Escapable` is also a **source-breaking** change. +conformance to Copyable or Escapable is also a **source-breaking** change. For example, if a library publishes this protocol: ```swift public protocol Manager: ~Copyable { @@ -611,7 +679,7 @@ public protocol Manager: ~Copyable { ``` Client code that states a `T: Manager` requirement on a generic parameter `T` can then assume that the type parameter -`T.Resource` is `Copyable`: +`T.Resource` is Copyable: ```swift extension Manager where Self: ~Copyable { func makeCopies(_ r: Self.Resource) -> (Self.Resource, Self.Resource) { @@ -626,8 +694,8 @@ public protocol Manager: ~Copyable { associatedtype Resource: ~Copyable } ``` -The client's extension of `Manager` will no longer type check, because -the body of `makeCopies()` assumes `r` is `Copyable`, and this +The client's extension of Manager will no longer type check, because +the body of `makeCopies()` assumes `r` is Copyable, and this assumption is no longer true. ## ABI Compatibility @@ -635,8 +703,24 @@ assumption is no longer true. The ABI of existing code is not affected by this proposal. 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. +to suppress conformance is can be an ABI-breaking change. For example, an +extension of a protocol providing a default implementation could have its symbol +name change, as these two implementations of `greet` must have distinct names: + +```swift +protocol Greeter { + associatedtype T: ~Copyable + func greet() +} + +extension Greeter { + func greet() { print("hello")} +} + +extension Greeter where T: ~Copyable { + func greet() { print("سلام") } +} +``` ## Future Directions @@ -660,10 +744,25 @@ constrained existential via `any Q`. ## Alternatives Considered +Through the development of this proposal, various alternate formulations were considered. + ### No recursion -A prior version of this proposal [was pitched](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880) that was absent of any defaulting behavior -for associated types. +A prior version of this proposal [was pitched](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880) that was absent of any defaulting behavior for associated types. The primary fault was that it +provided an inconsistent behavior when compared with generic types like S: + +```swift +struct S: ~Copyable {} + +protocol P: ~Copyable { + associatedtype T: ~Copyable +} + +extension S {} // T: Copyable +extension P {} // T: ~Copyable +``` + +Only the extension for S provides a default for its T. ### Definition-driven associated type defaults @@ -671,14 +770,14 @@ Rather than try to impose a blanket default on all primary associated types, we instead apply a limited defaulting rule only to select associated types, driven by some aspect of the protocol definition. This could come at the expense of increased language complexity. Readers would have to carefully consult the -definitions of protocols to see whether they come with default `Copyable` -or `Escapable` requirements on their associated types. +definitions of protocols to see whether they come with default Copyable +or Escapable requirements on their associated types. Some possibilities for how this might look include: #### 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. +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. ```swift protocol Container: ~Copyable, ~Escapable { @@ -691,9 +790,9 @@ protocol Container: ~Copyable, ~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. +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 noncopyable or +nonescapable conformers in practice. #### Default constraint sets @@ -722,7 +821,7 @@ extension Container without Copying, Escaping {} // fully unconstrained in -vers 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: +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: ```swift protocol Container: ~Copyable, ~Escapable, ~Runcible { From fb94bf11a01d354d58ab8b9baa93f49f483127ed Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Mon, 15 Dec 2025 15:07:00 -0800 Subject: [PATCH 6/7] No recursion -> No defaulting --- 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 2c88e7581e..a2636d5b3b 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -28,7 +28,7 @@ - [Future Directions](#future-directions) - [Constrained Existentials via `some`](#constrained-existentials-via-some) - [Alternatives Considered](#alternatives-considered) - - [No recursion](#no-recursion) + - [No defaulting](#no-defaulting) - [Definition-driven associated type defaults](#definition-driven-associated-type-defaults) - [Protocol-defined default requirements](#protocol-defined-default-requirements) - [Default constraint sets](#default-constraint-sets) @@ -746,7 +746,7 @@ constrained existential via `any Q`. Through the development of this proposal, various alternate formulations were considered. -### No recursion +### No defaulting A prior version of this proposal [was pitched](https://forums.swift.org/t/pitch-suppressed-default-conformances-on-associated-types/81880) that was absent of any defaulting behavior for associated types. The primary fault was that it provided an inconsistent behavior when compared with generic types like S: From f893e0c007b18663bd4d178d5b8da9f1905eccc3 Mon Sep 17 00:00:00 2001 From: Kavon Farvardin Date: Mon, 15 Dec 2025 15:25:26 -0800 Subject: [PATCH 7/7] Try to clarify some of the alternatives a bit --- proposals/NNNN-suppressed-associated-types.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/proposals/NNNN-suppressed-associated-types.md b/proposals/NNNN-suppressed-associated-types.md index a2636d5b3b..ef4fe2dbcb 100644 --- a/proposals/NNNN-suppressed-associated-types.md +++ b/proposals/NNNN-suppressed-associated-types.md @@ -787,6 +787,11 @@ protocol Container: ~Copyable, ~Escapable { default Element: Copyable, Element: Escapable } + +// defaults to 'where Element: Copyable & Escapable' only. +// +// Self and Self.BorrowingIterator remain ~Copyable & ~Escapable +extension Container {} ``` This might also serve as a way for a protocol to opt generic parameters *out* @@ -816,9 +821,11 @@ 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 {} +func f() without T: Container.Copying {} + +// We could have syntax that allows you to refer to constraintsets like a member, +// to opt out a generic type parameter from multiple constrainsets: +func g() without T: Container.Copying or T: P.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: