- Proposal: SE-0427
- Authors: Kavon Farvardin, Tim Kientzle, Slava Pestov
- Review Manager: Holly Borla, Ben Cohen
- Status: Implemented (Swift 6.0)
- Implementation: On
main
gated behind-enable-experimental-feature NoncopyableGenerics
- Previous Proposal: SE-0390: Noncopyable structs and enums
- Review: (pitch) (first review) (returned for revision) (second review) (acceptance)
Table of Contents
- Noncopyable Generics
The noncopyable types introduced in SE-0390: Noncopyable structs and enums cannot be used with generics, protocols, or existentials, leaving an expressivity gap in the language. This proposal extends Swift's type system to fill this gap.
Noncopyable structs and enums are intended to express value types for which it is not meaningful to have multiple copies of the same value.
Support for noncopyable generic types was omitted from SE-0390. For example,
Optional
could not be instantiated with a noncopyable type,
which prevented declaration of a failable initializer:
struct FileDescriptor: ~Copyable {
init?(filename: String) { // error: cannot form a Optional<FileDescriptor>
...
}
}
Practical use of generics also requires conformance to protocols, however noncopyable types could not conform to protocols.
In order to broaden the utility of noncopyable types in the language, we need a consistent and sound way to relax the fundamental assumption of copyability that permeates Swift's generics system.
We begin by recalling the restrictions from SE-0390:
- A noncopyable type could not appear in the generic argument of some other generic type.
- A noncopyable type could not conform to protocols.
- A noncopyable type could not be boxed as an existential.
This proposal builds on the ~Copyable
notation introduced in SE-0390, and
introduces three fundamental concepts that together eliminate these
restrictions:
- A new
Copyable
protocol abstracts over types whose values can be copied. - Every struct, enum, class, generic parameter, protocol and associated type
now conforms to
Copyable
by default. - The
~Copyable
notation is used to suppress this default conformance requirement anywhere it would otherwise be inferred.
Note: The adoption of noncopyable generics in the standard library will be covered in a subsequent proposal.
The notion of copyability of a value is now expressed as a special kind of
protocol. The existing ~Copyable
notation is re-interpreted as suppressing
a conformance to this protocol, as we detail below. This protocol has no
explicit requirements, and it has some special behaviors. For example,
metatypes and tuples cannot normally conform to other protocols,
but they do conform to Copyable
.
A key goal of the design is progressive disclosure. The idea of default conformance to
Copyable
means that a user never interacts with noncopyable generics unless
they choose to do so, using the ~Copyable
notation to suppress
the default conformance.
The meaning of existing code remains the same; all generic parameters and
protocols now require conformance to Copyable
, but all existing concrete
types do in fact conform.
Every struct and enum now has a default conformance to Copyable
, unless the
conformance is suppressed by writing ~Copyable
in the inheritance clause. In
this proposal, we will show these inferred requirements in comments. For example,
a definition of a copyable struct is understood as if the user wrote the
conformance to Copyable
:
struct Polygon /* : Copyable */ {...}
Furthermore, generic parameters now conform to Copyable
by
default, so the following generic function can only be called with Copyable
types:
func identity<T>(x: T) /* where T: Copyable */ { return x }
Finally, protocols also have a default conformance to Copyable
, thus
only Copyable
types can conform to Shape
below:
protocol Shape /*: Copyable */ {}
So far, we haven't described anything new, just formalized existing behavior with
a protocol. Now, we allow writing ~Copyable
in some new positions.
For example, to generalize our identity function to also allow noncopyable types, we
suppress the default Copyable
conformance on T
as follows:
func identity<T: ~Copyable>(x: consuming T) { return x }
This function imposes no requirements on the generic parameter T
. All possible
types, both Copyable
and noncopyable, can be substituted for T
.
This is the reason why we refer to ~Copyable
as suppressing the conformance
rather than inverting or negating it.
As with a concrete noncopyable type, a noncopyable generic parameter type must
be prefixed with one of the ownership modifiers borrowing
,
consuming
, or inout
, when it appears as the type of a function's parameter.
For details on these parameter ownership modifiers,
see SE-377.
A protocol can allow noncopyable conforming types by suppressing its inherited
conformance to Copyable
:
protocol Resource: ~Copyable {
consuming func dispose()
}
extension FileDescriptor: Resource {...}
A Copyable
type can still conform to a ~Copyable
protocol.
What it means to write ~Copyable
in each position will be fully explained in
the Detailed Design section.
This proposal does not fundamentally change the abstract theory of Swift
generics, with its four fundamental kinds of requirements that can appear in a
where
clause; namely conformance, superclass, AnyObject
, and same-type
requirements.
The proposed mechanism of default conformance to Copyable
, and its suppression by
writing ~Copyable
, is essentially a new form of syntax sugar; the transformation
is purely syntactic and local.
While Copyable
is a protocol in the current implementation, it is unlike a
protocol in some ways. In particular, protocol extensions of Copyable
are not
allowed:
extension Copyable { // error
func f() {}
}
Such a protocol extension would effectively add new members to every copyable type, which would complicate overload resolution and possibly lead to user confusion.
Default conformance to Copyable
is inferred in each position below,
unless explicitly suppressed:
- A struct, enum or class declaration.
- A generic parameter declaration.
- A protocol declaration.
- An associated type declaration; does not support suppression (see Future Directions).
- The
Self
type of a protocol extension. - The generic parameters of a concrete extension.
The ~Copyable
notation is also permitted to appear as the member of
a protocol composition type. This ensures that the following three declarations
have the same meaning, as one might expect:
func f<T: Resource & ~Copyable>(_: T) {}
func f<T>(_: T) where T: Resource & ~Copyable {}
func f<T>(_: T) where T: Resource, T: ~Copyable {}
A conformance to Copyable
cannot be suppressed if it must hold for
some other reason. In the above declaration of f()
, we can suppress
Copyable
on T
because Resource
suppresses its own Copyable
requirement
on Self
:
protocol Resource: ~Copyable {...}
Thus, nothing else forces f()
's generic parameter T
to be Copyable
. On the
other hand, let's look at a copyable protocol like Shape
below:
protocol Shape /*: Copyable */ {...}
If we try to suppress the Copyable
conformance on a generic parameter that also
conforms to Shape
, we get an error:
func f<T: Shape & ~Copyable>(_: T) {...} // error
The reason being that the conformance T: Copyable
is implied by T: Shape
, and
cannot be suppressed.
Furthermore, a Copyable
conformance can only be suppressed if the subject type
is a generic parameter declared in the innermost scope. That is, the following
is an error:
struct S<T /* : Copyable */> {
func f<U /* : Copyable */>(_: T, _: U) where T: ~Copyable // error!
}
The rationale here is that since S
must be instantiated with a copyable type,
it does not make sense for a method of S
to operate on an S<T>
where T
might be noncopyable. For a similar reason the same rule applies to nested
generic types.
We wish to allow existing types to adopt noncopyability without changing the
meaning of existing code. Thus, an extension of a concrete type must introduce
a default T: Copyable
requirement on every generic parameter of the
extended type:
struct Pair<T: ~Copyable>: ~Copyable {...}
extension Pair /* where T: Copyable */ {...}
The conformance can be suppressed to get an unconstrained extension of Pair
:
extension Pair where T: ~Copyable {...}
An extension presents a copyable view of the world by default, behaving as if
Pair
were declared like so:
struct Pair<T /* : Copyable */> /* : Copyable */ {...}
An extension of a nested type introduces default conformance requirements for all outer generic parameters of the extended type, and each conformance can be individually suppressed:
struct Outer<T: ~Copyable> {
struct Inner<U: ~Copyable> {}
}
extension Outer.Inner /* where T: Copyable, U: Copyable */ {}
extension Outer.Inner where T: ~Copyable /* , U: Copyable */ {}
extension Outer.Inner where /* T: Copyable, */ U: ~Copyable {}
An extension of a type whose generic parameters must be copyable cannot suppress conformances:
struct Horse<Hay> {...}
extension Horse where Hay: ~Copyable {...} // error
Where possible, we wish to allow the user to change an existing protocol to accommodate noncopyable conforming types, without changing the meaning of existing code.
For this reason, an extension of a ~Copyable
protocol also introduces a default
Self: Copyable
requirement, because this is the behavior expected from
existing clients:
protocol EventLog: ~Copyable {
...
}
extension EventLog /* where Self: Copyable */ {
func duplicate() -> Self {
return copy self // OK
}
}
To write an unconstrained protocol extension, suppress the conformance on Self
:
extension EventLog where Self: ~Copyable {
...
}
Associated types cannot have their Copyable
requirement suppressed
(see Future Directions).
Another consequence that immediately follows from the rules as explained so far
is that protocol inheritance must re-state ~Copyable
if needed:
protocol Token: ~Copyable {}
protocol ArcadeToken: Token /* , Copyable */ {}
protocol CasinoToken: Token, ~Copyable {}
Again, because ~Copyable
suppresses a default conformance instead of introducing
a new kind of requirement, it is not propagated through protocol inheritance.
Structs and enums conform to Copyable
unconditionally by default, but a
conditional conformance can also be defined. For example, take this
noncopyable generic type:
enum List<T: ~Copyable>: ~Copyable {
case empty
indirect case element(T, List<T>)
}
We would like List<Int>
to be Copyable
since Int
is, while still being
able to use a noncopyable element type, like List<FileDescriptor>
. We do
this by declaring a conditional conformance:
extension List: Copyable where T: Copyable {}
Note that the where
clause needs to be written, because a conformance to
Copyable
declared in an extension does not automatically add any other
requirements, unlike other extensions.
A conditional Copyable
conformance is not permitted if the
struct or enum declares a deinit
. Deterministic destruction requires the
type to be unconditionally noncopyable.
A conformance to Copyable
is checked by verifying that every stored property
(of a struct) or associated value (of an enum) itself conforms to Copyable
.
For a conditional Copyable
conformance, the conditional requirements must be
sufficient to ensure this is the case. For example, the following is rejected,
because the struct cannot unconditionally conform to Copyable
, having a
stored property of the noncopyable type T
:
struct Holder<T: ~Copyable> /* : Copyable */ {
var value: T // error
}
There are two situations when it is permissible for a copyable type to have a noncopyable generic parameter. The first is when the generic parameter is not stored inside the type itself:
struct Factory<T: ~Copyable> /* : Copyable */ {
let fn: () -> T // ok
}
The above is permitted, because a function of type () -> T
is still copyable,
even if a value of type T
is not copyable.
The second case is when the type is a class. The contents of a class is never copied, so noncopyable types can appear in the stored properties of a class:
class Box<T: ~Copyable> {
let value: T // ok
init(value: consuming T) { self.value = value }
}
For a conditional Copyable
conformance, the conditional requirements must be
of the form T: Copyable
where T
is a generic parameter of the type. It is
not permitted to make Copyable
conditional on any other kind of requirement:
extension Pair: Copyable where T == Array<Int> {} // error
Conditional Copyable
conformance must be declared in the same source
file as the struct or enum itself. Unlike conformance to other protocols,
copyability is a deep, inherent property of the type itself.
This proposal supports classes with noncopyable generic parameters,
but it does not permit classes to themselves be ~Copyable
.
Similarly, an AnyObject
or superclass requirement cannot be combined with
~Copyable
:
func f<T>(_ t: T) where T: AnyObject, T: ~Copyable { ... } // error
The type Any
is no longer the supertype of all types in the type system's
implicit conversion rules.
The constraint type of an existential type is now understood as being a
protocol composition, with a default Copyable
member. So
the empty protocol composition type Any
is really any Copyable
, and the
supertype of all types is now any ~Copyable
:
any ~Copyable
/ \
/ \
Any == any Copyable <all purely noncopyable types>
|
<all copyable types>
This default conformance is suppressed by writing ~Copyable
as a member of a
protocol composition:
protocol Pizza: ~Copyable {}
struct UniquePizza: Pizza, ~Copyable {}
let t: any Pizza /* & Copyable */ = UniquePizza() // error
let _: any Pizza & ~Copyable = UniquePizza() // ok
The default conformance to Copyable
is inferred anywhere it is not explicitly
suppressed with ~Copyable
, so this proposal does not change the interpretation
of existing code.
Similarly, the re-interpretation of the SE-0390 restrictions in terms of
conformance to Copyable
preserves the meaning of existing code that makes use of
noncopyable structs and enums.
This proposal does not change the ABI of existing code.
Adding ~Copyable
to
an existing generic parameter is generally an ABI-breaking change, even when
source-compatible.
Targeted mechanisms are being developed to preserve ABI compatibility when
adopting ~Copyable
on previously-shipped generic code. This will enable adoption
of this feature by standard library types such as Optional
. Such mechanisms will
require extreme care to use correctly.
The spelling of ~Copyable
generalizes the existing syntax introduced in
SE-0390, and changing it is out of scope for this proposal.
A simple design for suppressed associated types was considered, where the
default conformance in a protocol extension applies only to Self
, and not
the associated types of Self
. For example, we first declare a protocol with a
~Copyable
associated type:
protocol Manager {
associatedtype Resource: ~Copyable
}
Now, a protocol extension of Manager
does not carry an implicit
Self.Resource: Copyable
requirement:
extension Manager {
func f(resource: Resource) {
// `resource' cannot be copied here!
}
}
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.
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:
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:
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:
struct ManagerManager<T: Manager>: ~Copyable {}
extension ManagerManager: Copyable where T.Resource: Copyable {} // error
This design for associated types was initially implemented but ultimately removed from this proposal, because of the source compatibility issues. A more comprehensive design that allows for some way of preserving source compatibility requires a separate proposal due to the open design issues.
A struct or enum can opt out of copyability with ~Copyable
, and then possibly
declare a conditional conformance. It would be possible to automatically infer
this conditional conformance. For example, in the below,
struct MaybeCopyable<T: ~Copyable> {
var t: T
}
The only way this could be valid is if we had inferred the conditional conformance:
extension MaybeCopyable: Copyable /* where T: Copyable */ {}
Feedback from early attempts at implementing this form of inference suggested it was more confusing than helpful, so it was removed.
One possible downside is that extensions of types with noncopyable generic parameters must suppress the conformance on each generic parameter.
It would be possible to allow library authors to explicitly control this
behavior, with a new syntax allowing the default where
clause of an
extension to be written inside of a type declaration. For example,
public enum Either<T: ~Copyable, U: ~Copyable> {
case a(T)
case b(U)
// Hypothetical syntax:
default extension where T: Copyable, U: ~Copyable
}
// `T` is copyable, but `U` is not, because of the defaults above:
extension Either /* where T: Copyable */ { ... }
This becomes much more complex for protocols that impose conformance requirements on their own associated types:
protocol P: ~Copyable {
associatedtype A: P, ~Copyable
// Hypothetical syntax:
default extension where A: Copyable
}
extension P {
// A is Copyable. What about A.A? A.A.A? ...
}
Besides the unclear semantics with associated types, it was also felt this
approach could lead to user confusion about the meaning of a particular
extension. As a result, we feel that explicitly suppressing Copyable
on
every extension is the best approach.
The behavior of default Copyable
conformance on associated types prevents
existing protocols from adopting ~Copyable
on their associated types in a
source compatible way.
For example, suppose we attempt to change IteratorProtocol
to accommodate
noncopyable element types:
protocol IteratorProtocol: ~Copyable {
associatedtype Element: ~Copyable
mutating func next() -> Element?
}
An existing program might declare a generic function that assumes T.Element
is
Copyable
:
func f<T: IteratorProtocol /* & Copyable */>(iter: inout T) {
let value = iter.next()!
let copy = value // error
}
Since IteratorProtocol
suppresses its Copyable
conformance, the generic
parameter T
defaults to Copyable
. However, T.Element
is no longer
Copyable
, thus the above code would not compile.
One can imagine a design where instead of a single default conformance
requirement T: Copyable
being introduced above, we also add a requirement
T.Element: Copyable
. This would preserve source compatibility and our
function f()
would continue to work as before.
However, this approach introduces major complications, if we again consider protocols that impose conformance requirements on their associated types.
Consider this simple protocol and function that uses it:
protocol P: ~Copyable {
associatedtype A: P, ~Copyable
}
func f<T: P>(_: T) {}
Our hypothetical design would actually introduce an infinite sequence of requirements here unless suppressed:
func f<T: P>(_: T) /* where T: Copyable, T.A: Copyable, T.A.A: Copyable, ... */ {}
Of course, it seems natural to represent this infinite sequence of requirements as a new kind of "recursive conformance" requirement instead.
Swift generics are based on the mathematical theory of string rewriting, and requirements and associated types define certain rewrite rules which operate on a set of terms. In this formalism, a hypothetical "recursive conformance" requirement corresponds to a rewrite rule that can match an infinite set of terms given by a regular expression. We would then need to generalize the algorithms for deciding term equivalence to handle regular expressions. While there has been research in this area, the design for such a system is far beyond the scope of this proposal.
Instead of the syntactic desugaring presented in this proposal, one can attempt to
formalize T: ~Copyable
as the logical negation
of a conformance, extending the theory of Swift generics with a fifth requirement kind to
represent this negation. It is not apparent how this leads to a sound and
usable model and we have not explored this further.
Supporting the full generality of associated types with suppressed Copyable requirements, while providing a mechanism to preserve source compatibility is a highly desirable goal. At the same time, it is a large, open design problem. A few ideas were considered (see Alternatives Considered) but it was ultimately determined to be too complex to tackle in this proposal.
The Optional
and UnsafePointer
family of types can support noncopyable types
in a straightforward way. In the future, we will also explore noncopyable
collections, and so on. All of this requires significant design work and is out
of scope for this proposal.
Noncopyable tuples and parameter packs are a straightforward generalization which will be discussed in a separate proposal.
The ability to "escape" the current context is another implicit capability of all current Swift types. Suppressing this requirement provides an alternative way to control object lifetimes. A companion proposal will provide details.
Thank you to Joe Groff and Ben Cohen for their feedback throughout the development of this proposal.