SE-0427: Noncopyable Generics

I mean, I think @wadetregaskis explanation is very helpful for understanding, but I actually think the syntax is fine in the long run given what's changing.

If something is flagged as ~Copyable the compiler will add what will feel like new and additional restrictions to the behavior of that item. What's actually happening is that language has changed and what happens in ~Copyable conforming code is actually the new default and it's the Copyable stuff that's giving you special features. (As seen in the proposal) Expressing the removal/suppression of features has to be done with two protocols at the moment, because that's what the language has to do it. (Do I have this correct?)


              any ~Copyable
               /         \
              /           \
   Any == any Copyable   <all purely noncopyable types>
        |
<all copyable types>

Introducing a suppression of protocols syntax AND doing with something that wasn't even a protocol before and gets automagically assigned to things... whew. It's going to hurt, but I don't agree that it was wrong.

3 Likes

‘~Copyable’ is not a protocol. It’s the absence of the Copyable protocol. It doesn’t make sense to say “conforming to ~Copyable”. You don’t conform to nothing.

This proposal is about changing the grammar of generics, so you can state the absence (or suppression) of one specific protocol, Copyable, as a requirement of a generic parameter.

You’re requiring the absence of Copyable, and when that can’t be satisfied (because maybe some other requirement needs Copyable) then you get an error from the type checker. For example, this is an error: ‘Copyable & ~Copyable’

T: P means “T is required to satisfy P”. And T: ~Copyable means “T is required to not have a Copyable requirement”.

Similarly, ‘some ~Copyable’ is some generic value without a Copyable requirement.

Once a type does not satisfy Copyable, you can’t copy it. Thus if T: ~Copyable is true, then T is noncopyable.

3 Likes

I honestly really do get that that's the end goal. And I'm all for it.

But right now

  • ~Copyable was introduced into the language as a usable thing before Copyable will have been
  • It appeared to introduce new behaviors
  • Copyable will be largely invisible to the average user so talking about its absence while insisting no one will have to notice it's existence feels less than illuminating.
  • it goes in the same place as a protocol
  • There will (at least temporarily currently) be no such thing as a concrete type that will be allowed to be declared as neither :Copyable or :~Copyable, (see the changes to any) although much of that will again be invisible. Which if ~Copyable was genuinely the absence of Copyable that wouldn't be needed for concrete types (protocols and generics, yes, but not concrete), Copyable could just be left off. But you can't just do nothing because something needs to block the Compiler from making the super secret Copyable conformance. Which I understand the reason for, but for the folks who were told "don't worry your pretty little head about what's behind the curtain it doesn't need to bother you" it's another "Wait, what?"

I honestly don't think just repeatedly insisting that something that looks like that duck, walks like that duck and quacks like that duck isn't that duck gets people where they need to be.

But I will with all deference stop using the phrase "conform to ~Copyable."

I hope this proposal or something near to it passes because I think the direction is exciting.

I also think its going to take more than one chicken-island-pineapple WWDC video for people to sort it out.

Mmm...not quite: it means that T is not required to have a Copyable requirement.

5 Likes

I believe that @kavon's point is that specifying ~Copyable is actually saying "this type param must not have a Copyable requirement, and that if such a requirement is inferred then that's a conflict", if I'm reading this correctly:

If ~Copyable only meant "not required to be Copyable" then Copyable & ~Copyable would be silly, but not an error. Of course, the downstream effect for callers of "T must not have a Copyable requirement" is "whatever you substitute for T is not required to be Copyable".

Requiring that something can't be copied doesn't really make sense though, does it?

Even if a thing can be copied (ie it is Copyable), you don't have to copy it. Why would you ever want to require that copying must be impossible?

I've always understood ~Copyable as Xiaodi described it: that copyability is not required.

7 Likes

I don't really think we're disagreeing, just talking about slightly different viewpoints. The way I am thinking about the distinction being drawn here is as between an 'internal' view of the T: ~Copyable declaration, i.e., "what can I do with a value of type T?" and the 'external' view of T: ~Copyable as a potential target for a client's generic substitution, i.e. "what types can be passed for T?".

Externally, T: ~Copyable allows substitution for types which may or may not be copyable for exactly the reason you mention: the ~Copyable constraint only removes capabilities so a type which is, in fact, Copyable of course satisfies such a constraint.

Internally, though, T: ~Copyable is indeed a requirement that values of type T cannot formally be copied within the implementation of the relevant function or type. It couldn't be any other way--if copies were allowed in the implementation then it wouldn't be possible to substitute some concrete non-copyable type for T.

As a practical matter it would be irritating if you could declare T: ~Copyable but then inadvertently reintroduce a T: Copyable requirement due to, e.g., the transitive effect of some other constraint in a where clause: the author has explicitly declared their intent that T should not be required to be Copyable and so it's an error if something else reintroduces a requirement that was explicitly suppressed.

3 Likes

The behavior you describe is implemented by making a second pass over the suppressed requirements after the generic signature has been built, and diagnosing any that are “nevertheless true”. This is purely artificial though, and we didn’t do that we would accept “Copyable & ~Copyable” without any inconsistency. So it’s still not really a requirement, even with the fancy diagnostic check.

So if a protocol P is Copyable, then where T: P, T: ~Copyable and where T: P define the same generic signature; the latter also had a T: Copyable, which was just redundant so it got deleted.

4 Likes

Right, there's no soundness issue with Copyable & ~Copyable, but preventing it keeps authors from accidentally writing signatures which purport to suppress the Copyable requirement, but in fact introduce the requirement via some other constraint (perhaps accidentally).

5 Likes

Because there is no soundness issue, my feedback re this part of the proposal—which I had meant to post earlier but neglected—is that this artificial diagnostic pass is brilliant but ought to result in a warning and not an error, precisely because it encourages an inconsistent understanding of the semantics of ~Copyable.

It is perfectly well defined what Copyable & ~Copyable means in Swift and to the compiler: in cases where Copyable is inferred, it can well be misleading to the human reader, but in the explicit case it really just is silly, which is not grounds for refusing to compile at all in my view.

8 Likes

I'd like to place a vote for no longer using "non-copyable type" or anything along the lines of calling ~Copyable "a constraint" or a "requirement", too?

~SomeProtocol meaning

  • a type that does not conform to SomeProtocol
  • a type that's not SomeProtocol
  • a protocol/generic that does not required a type to be SomeProtocol
  • a protocol or generic that promises not to use the features of SomeProtocol
  • the ~SomeProtocol release, the ~SomeProtocol freeing of the SomeProtocol constraint

ETA: Okay perhaps I take the "a constraint" or a "requirement" part back in light of the below, but just saying "noncopying type" is at this stage in the game too vague for those of us in the peanut gallery.

  • a type that does not conform to SomeProtocol (too weak for labeled ~Copyable?)
  • a type that's not SomeProtocol (too weak? for labeled ~Copyable?)
  • a type/protocol/generic that disallows SomeProtocol behavior in itself (and all extensions ?)
  • a prohibited SomeProtocol
  • a protocol or generic that promises not to use the features of SomeProtocol
  • the ~SomeProtocol suppression, the ~SomeProtocol quash of SomeProtocol constraint

No. It really does mean that T cannot have a Copyable requirement. In other words, it requires the absence of Copyable. There's no "maybe" about it. It's a subtle difference but an important one that I will demonstrate...

But it is an intentional and crucial part of the design I'm proposing that we make ~Copyable act as though it is a requirement in the Swift language for users. The fact that it's artificial in the current implementation does not matter.

I will not relent on it being an error without some very convincing reasoning!

The model is absolutely nonsensical if I can write T: Copyable & ~Copyable and my code still compiles with just a warning. With the ability to copy values of type T, despite having written ~Copyable, turns that syntax into just a suggestion. It should be a requirement that T isn't Copyable because that's what the user is requesting.

In fact, my goal in this work has been that people can reliably write ~Copyable on a type T anywhere, and it can always be consistently summarized as thinking "T is absent of a Copyable requirement". That aligns with the existing usage of ~Copyable as well.

Here are all of the places where ~Copyable can be written, with the currently implemented diagnostics, to demonstrate why I keep saying that it requires the absence of Copyable:


  1. Inheritance clauses for concrete types.
protocol P {}
// note: type 'S' does not conform to inherited protocol 'Copyable'

struct S: ~Copyable, P {}
// error: type 'S' does not conform to protocol 'Copyable'

Here, the protocol P implicitly inherits from Copyable because it did not opt-out of requiring it. So its conformers must also conform to Copyable. Since S opted out of conforming to Copyable via ~Copyable, S cannot conform to P.

Thus, writing ~Copyable on S most definitely made it not be required to be Copyable. In fact, if I try:

struct S: ~Copyable {}

extension S: Copyable {}
// error: struct 'S' required to be 'Copyable' but is marked with '~Copyable'

I can't add it back with this extension, since there's nothing for it to be conditional on! And an unconditional extension adding Copyable when it was required to not be Copyable is nonsense. Even if S were generic, I still can't add it back if T is already Copyable, because all conditions are then already satisfied:

struct S<T>: ~Copyable {}

extension S: Copyable {} 
// error: generic struct 'S' required to be 'Copyable' but is marked with '~Copyable'

  1. Inheritance clauses of generic nominal types
struct S<T: ~Copyable>: ~Copyable {}

Here, T does not require Copyable, and the family of types S<T> is not Copyable. Just like how the family of types S<T> is not Equatable. Now, if I also have this extension (which is required to be in the same source-file as S:

extension S: Copyable where T: Copyable {}

The family of types S, in general, is still not Copyable. There definitely exists a type you can substitute into S<T> to make it noncopyable.

This extension just adds the condition that "S is Copyable when T is Copyable". You're totally welcome to write that out, and the compiler won't complain about the redundant where clause.

Keep in mind that this is exactly like how the following extension adds the condition "S is Equatable when T is Equatable":

extension S: Equatable where T: Equatable {}

Try as I might, I still cannot make the entire family of S<T> Copyable, despite being marked ~Copyable! The compiler catches this conflicting extension too! :slight_smile:

struct S<T: ~Copyable>: ~Copyable {}

extension S: Copyable where T: ~Copyable {}
// error: generic struct 'S' required to be 'Copyable' but is marked with '~Copyable'

  1. Inheritance clauses for protocols
protocol P {}

protocol Q: ~Copyable, P {}
// error: 'Self' required to be 'Copyable' but is marked with '~Copyable'

Here, Q tries to inherit from P, which requires Copyable, but Q's Self is also required to not require Copyable, because of the ~Copyable in its inheritance clause.

That boils down to Copyable & ~Copyable: two conflicting requirements that indicates the programmer made a mistake. If the compiler were to only warn about Q actually requiring it's conformers to be Copyable, despite programmer's explicit marking, that would be terribly confusing.


  1. where clauses

This is where most of the complexity can arise, due to how Swift's generics system works. But you still can rely on ~Copyable requiring that a generic parameter does not have a Copyable requirement...

Example 1:

protocol P {}

func f<T>(_ t: T) where T: ~Copyable, T: P {}
// error: 'T' required to be 'Copyable' but is marked with '~Copyable'

Similar to the inheritance clause examples, T here was required to not require Copyable, yet in order to satisfy the requirements of P, which implicitly inherits from Copyable, T would have to be Copyable. This again boils down to Copyable & ~Copyable.

Example 2:

struct R<V> {}

func f<T: ~Copyable>(_ r: R<T>) {}
// error: 'T' required to be 'Copyable' but is marked with '~Copyable'

Here, R's generic parameter V did not opt out when it was defined, so it requires Copyable. In function f, T is required to not require Copyable. But once we attempt to substitute T in for V, we get an error, because in Swift we infer requirements for T based on its substitution here inside R.

It would be very confusing if T were still Copyable here, or only warned about. Who's requirement really wins? Does V's Copyable win or T's ~Copyable? Neither, it's an error!

Example 3:

protocol P {
  associatedtype Element
}

func f<T, U>(_ t: T, _ u: U)
  where
    U: ~Copyable, // error: 'U' required to be 'Copyable' but is marked with '~Copyable'
    T: P,
    U == T.Element
  {}

This one is more subtle, but the compiler has your back! You wanted to require U to not require Copyable, but you've used a same-type requirement that implies U must be Copyable, because the associatedtype Element has not opted-out, and thus requires Copyable.

Example 4:

class Soup {}
func f<T>(_ t: T) where T: ~Copyable, T: Soup {}
// error: 'T' required to be 'Copyable' but is marked with '~Copyable'

Here, you've required T to not require Copyable, but you've also placed a class bound on T, which means only classes that are a subtype of the class Soup can be substituted. But all classes require Copyable, thus you haven't actually made it such that a noncopyable type can be substituted for T at all! The same principle underlies the rejection of AnyObject & ~Copyable.


  1. Existentials

This is sort of a freebie, because existentials can only have a ~Copyable in it if the existential is some composition. Thus, we have the same set of rules here, as if the composition A & B were broken up into a where clause like Self where Self: A, Self: B.

protocol P {}
protocol Q: ~Copyable {}
class Soup {}

let _: any P & ~Copyable // error: composition cannot contain '~Copyable' when another member requires 'Copyable'

let _: any AnyObject & ~Copyable // error: composition involving 'AnyObject' cannot contain '~Copyable'

let _: any Soup & ~Copyable // error: composition involving class requirement 'Soup' cannot contain '~Copyable'

let _: any Q & ~Copyable // OK
9 Likes

For what it's worth, this would still be an error with @xwu's proposed change. A concrete type not satisfying the requirements of a protocol is a fundamentally different issue, and necessarily fatal.

This is also interesting if you consider AnyObject instead of Soup:

func f<T: AnyObject & ~Copyable>(_: T) {}

This is totally fine! Except that today all classes are copyable so this is the same as T: AnyObject. But maybe it won't be in the future? Error or warning?

I think the difficulty is that while this makes sense from the internal view of T where it is a bona fide type which cannot be copied, this framing is at odds with external users of T approaching it as a target for generic substitution. Theses users must view T: ~Copyable as “T may be substituted with any type, regardless of copyability.” To read it as “T is required to not be Copyable” as an external user suggests something which is not true. So I don’t think we can escape some notion of ~Copyable being read as “not required to be Copyable”, at least in some circumstances.

That said, I’m not totally sold on Xiaodi’s suggestion that it just be a warning. While a warning might be suitable for the author of a generic type using ~Copyable, if it is possible for ~Copyable to appear in a generic signature despite the generic parameter formally requiring Copyable (because, perhaps a library author was sloppy and let a warning slip through), it will also no longer be possible for clients to reliably read ~Copyable as “does not require Copyable”—there may indeed be types marked ~Copyable which nonetheless are in fact required to be Copyable!

6 Likes

Yes I agree that there’s two ways to view it, from the user of an interface and its implementation.

I admittedly have been a little sloppy in that last post, but “T is required to not require Copyable” is a sort of double-negative for saying “T is absent of Copyable”.

Absence of a requirement does suggest to people that you can put anything in there regardless of copyability. It also means that it lacks the capability to be copied, so it’s noncopyable. So does it make sense to anyone to read ‘~Copyable’ as ‘absent Copyable’ ?

1 Like

Help me square the above with this from the proposal:

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.

What’s in the proposal scans to me pretty clearly as ‘maybe copyable’ which is very different in my mind from ‘absent Copyable’.

2 Likes

This is why the abstraction here is slightly leaky. I don’t think there’s a way to fully explain substitution without appealing to the internal “desugared” list of requirements, that already had the defaults and suppression applied, which is what the proposal does when it spells out the three rules:

If this was a math textbook, the above would be all you’d get (and the algorithm for detecting spurious ~Copyable might be an exercise :grinning:); the rest of the proposal is an immediate logical consequence of these rules.

5 Likes

This is irreconcilable with the notion that any ~Copyable—which, since we are deferring mandatory existential any, would also just be plain ~Copyable—is the supertype of all types.

That ~Copyable is not a requirement is plain because, if it were, it would make no sense that this proposal would have it that I can write var x: ~Copyable; x = [1, 2, 3].

The spelling and semantics of ~Copyable as “not required to be Copyable”—and the decision that “required not to be Copyable” would not be utterable—was settled during the review process for the original proposal for noncopyable types; it’s unclear why this proposal is revisiting that decided point.

1 Like

Yes, that is an excellent point. That could argue for a middle ground where such misleading signatures are forbidden in public declarations (in the same spirit in which we require explicit access modifiers and refuse to synthesize memberwise initializers for public APIs) without being so severe with, say, local declarations. That is a bit convoluted, admittedly, and an across-the-board error is probably the best suited to address your point.

That said, I am concerned that we do not have consensus on the meaning of suppressed Copyable; having rejected a distinct indicator for unconditionally noncopyable types, then having one spelling pull double duty as both “not required to be” and “required not to be” was very much not the intention as I understand it.

6 Likes

Yeah, if we concede that the problem is severe enough to warrant an error across module boundaries then I don't see a compelling reason to allow such constructions internally. I view it as similar to the prohibition on constraints which make generic declarations effectively non-generic, which are slated to be errors in Swift 6:

func f<T>(_: T) where T == Int {} // this is an error in Swift 6

I do not read the diagnostic we're discussing as re-litigating this point, nor as inconsistent with:

I agree with Slava that calling this a 'requirement' is misleading because it is not generic requirement in the precise, formal sense used when discussing generics. The sense in which it is a requirement is more colloquial: it is a 'requirement' of non-misleadingness within the scope of the type declaration. One can read a type declaration with a ~Copyable constraint suppression and be confident that the implicit Copyable constraint is indeed suppressed.

To phrase things slightly differently: ~Copyable is not a requirement in that it imposes any additional restrictions on the underlying types/values to which it applies (indeed, it only relaxes such restrictions). Rather, it is a sort of meta-requirement on the type declaration itself, that once ~Copyable has appeared in source, no other part of the type signature may contradict that explicit suppression (implicitly or explicitly). This property is what will allow readers of a type declaration to consistently understand ~Copyable as "has had the implicit Copyable conformance suppressed".

7 Likes