Hi all,
I’m seeking feedback on a new language feature. I’ve included the introduction and proposed solution below, see Full Pitch Link for all the details.
Introduction
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:
/// Queue has no reason to require Element to be Copyable.
protocol Queue<Element>: ~Copyable {
associatedtype Element
associatedtype Allocator = DefaultAllocator
init()
init(alloc: Allocator)
mutating func push(_: Element)
mutating func pop() -> Element
// ...
}
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 type conforming to Queue is permitted to be noncopyable, its Element type has to be Copyable:
/// error: LinkedList does not conform to Queue
/// note: Element is required to be Copyable
struct LinkedList<Element: ~Copyable>: ~Copyable, Queue {
...
}
This is because in SE-427: Noncopyable Generics, 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
The existing syntax for suppressing these default conformances is extended to associated type declarations:
/// Correct Queue protocol.
protocol Queue<Element>: ~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 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. Unless otherwise noted, any discussion of ~Copyable types applies equivalently to ~Escapable types in this proposal.
Defaulting Behavior
Swift's philosophy behind defaulting 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 proposed defaulting behavior for associated types.
Here is a simplistic protocol for a Buffer that imposes no Copyable requirements:
protocol Buffer<Data>: ~Copyable {
associatedtype Data: ~Copyable
associatedtype Parser: ~Copyable
...
}
Recall the existing rules from SE-427: Noncopyable Generics. 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 (non-primary) associated type Parser, when constraining the generic parameter B to conform to Buffer:
// by default, B: Copyable, B.Data: Copyable
func read<B: Buffer>(_ 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:
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,
struct BinaryParser: ~Copyable { ... }
struct TextParser { ... }
class DecompressingReader<Data>: Buffer {
typealias Parser = BinaryParser
}
struct Reader<Data>: 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.