SE-0427: Noncopyable Generics

I think this will work just fine and is orthogonal to the extension discussion.

We already are proposing that inheritance of ~Copyable in protocols works basically as you describe for classes. That is, this would be legal:

class Base: ~Sendable {}
class Derived: Base, Sendable {}

But this would be diagnosed as an error:

class Base: Sendable {}
class Derived: Base, ~Sendable {}
1 Like

Yeah, fair point. It's similar, actually, to how isolation constraints can be secretly applied through protocol conformances, and how actors have numerous problematic interactions of that nature with protocols broadly. I certainly wish those didn't work that way; it's caused real problems for me and many others.

That could be addressed (in this Copyable case) with the requirement to also explicitly conform to Copyable in the same extension where it conforms to a Copyable-requiring type - which I think genuinely is a reasonable approach, conceptually, and is what should be required more broadly for protocol conformance side-effects like this. But I [now] concur it's also reasonable to discourage overriding ~Copyable (source-compatibility notwithstanding, I'd still prefer it start as a warning though).

3 Likes

It would be similarly problematic, I think, if we allowed this kind of "hidden" copyable conformance even if it were conditional:

struct S<T>: ~Copyable {}

extension S: P where T == Int {}
// oops, accidentally made S conditionally copyable,
// precluding the future addition of noncopyable fields in S

Maybe we should require any conformance of a noncopyable type to Copyable, even if implicit, to explicitly name Copyable.

struct S<T>: ~Copyable {}

extension S: P, Copyable where T == Int {}

This would also be a step towards letting protocols retroactively support noncopyable types, killing two birds with one stone.

5 Likes

This is where I think that my proposed alternative inference rule shines if applied across the board:

If we always infer where Self: Copyable on extensions regardless whether of protocols or of concrete types (unless the extension is itself an explicit conformance to Copyable, of course, or suppresses the inference with where Self: ~Copyable), then the concern here is well addressed (and separable from the original issue being discussed of explicit Copyable conformances):

extension S: P /* where Self: Copyable */ { }
// error: 'S' is never 'Copyable'

I'm not concerned about this at all. It is redundant like Equatable, Hashable is redundant, and if desired we can diagnose it for redundancy. But ultimately, this is a stylistic faux pas, and there are plenty of nauseating stylistic choices out there in Swift code upon which we do not bring down the hammer of a compiler error.


I would like us to be cautious about overfitting here to the two (relatively) recently introduced protocols which both have no implementation requirements.

Just as we've said above that the concept of "marker protocols" now (regrettably) in hindsight seems to have been overfitted to Sendable, I worry that we are creating rules for ~ that do not generalize even as we've intimated that we'd like to use the same spelling down the road for suppressing inferred conformances to Equatable, Hashable, etc.

Given a protocol like Copyable and a non-generic type, the distinction between "suppressing the default conformance" and the meta-requirement "requiring not conforming" is close to none for most purposes. This is in no small part because there isn't more than one way to conformā€”i.e., there is no implementation to give. It is easy, then, to say that the originally introduced semantics of ~Copyable in the prior proposal is equivalent to a meta-requirement and that it makes no sense to write struct S: ~Copyable and then extension S: Copyable.

By contrast, given a protocol such as Codable, it would very much make sense for a type to want to suppress the inferred default conformance and to later, in an extension grouping all the implementation details, adopt a custom conformance. For that reason, it makes a great deal of difference whether struct T: ~Codable means (to the human readerā€”I am not suggesting that there cannot be a consistent set of rules to make the compiler DWIM) that T is simply suppressing the inferred default conformance or instead adopting a meta-requirement never to conform.

6 Likes

That's part of what still bothers me - that there's no syntactic distinction between the two "classes" of protocols. It requires the reader to know a priori, somehow, that this protocol behaves one way while that protocol behaves another. That makes the language less intuitive (it's an arbitrary distinction that requires memorisation) and harder to learn (more complexity).

Of course, it's not a big deal, as long as the set of 'weird' protocols is very small and changes infrequently, but it's not ideal.

The design already limits Copyable conformances to only be conditional on conformances to Copyable, so this same-type requirement in particular isn't legal. But I get the general thing you're getting at here, so let me rephrase it as this:

protocol P {}

struct S<T>: ~Copyable {}

extension S: P where T: Copyable {}

We could say that this is an error, because conditional conformances for Copyable itself must not be implied, and instead be explicitly written in a separate extension, similar to what we have today for conditional conformances:

protocol Q {}
protocol P: Q {}

struct S<T> {}
extension S: P where T == String {}
// error: conditional conformance of type 'S<T>' to protocol 'P' does not imply conformance to inherited protocol 'Q'
// note: did you mean to explicitly state the conformance like 'extension S: Q where ...'?

We have to allow a conditional conformance to P imply conformance to Q if the where clause only contains requirements to conform to Copyable, i.e.,

struct S<T: ~Copyable> {}

extension S: P where T: Copyable {}
// S also conforms to Q

in order to allow for progressive disclosure and the retrofitting of existing types to allow noncopyable values in it. Because that where T: Copyable is implicit in every extension of S unless you state T: ~Copyable again.

But, we can make an exception to that if the implied conformance is for Copyable itself. Then, you need to write the extension out, so that there's no questions about when the type is Copyable:

protocol Q {}
protocol P: Q {}

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

extension S: Copyable {} // conditional conformance to Copyable
extension S: P {} // includes conditional conformance to Q, not Copyable.

The S in this example is not generic, so if you meant where T: Copyable here, then this should not be an error. We have no other syntax for writing conditional conformances than extensions.

~Copyable does not mean "never Copyable" in the proposed design. It means it's never unconditionally Copyable.

I've now given numerous examples that demonstrate that Copyable & ~Copyable is confusing, illogical, and debases the meaning of ~Copyable to a mere suggestion. It absolutely deserves to be an error from the outset and that cannot be hand-waved away as just stylistic. We surely must diagnose this as an error because Copyable is implied:

protocol P {}
struct S: P, ~Copyable {}

So why should we have a special case to allow struct S: Copyable, ~Copyable {}?

4 Likes

How can I declare a generic function that allows move-only (~Copyable) values and decline Copyable-values passed as and argument?

The example from proposal:

func identity<T: ~Copyable>(x: consuming T) { return x } {}

This function accepts both Copyable and ~Copyable types. But how can I restrict it to move-only types?

1 Like

Another one confusing aspect is any ~Copyable. If I understand the proposal correctly, it is possible to assign to any ~Copyable variable both Copyable and ~Copyable values. But there are move only types which have different bahaviour and constraints, so I expect any ~Copyable variable can only contain move-only value, which might have deinit.

1 Like

You can't. What is the purpose of doing that? Generics are about stating the minimum requirements so that your function is maximally general. So there isn't a way in Swift to exclude values unless if it lacks a capability that you need. At best, you can rely on overloading to provide a specific implementation if it supports a capability that isn't strictly required:

func identity<T: ~Copyable>(x: consuming T) -> T { return x }

// Overload for 'identity' for Copyable values.
func identity<T>(x: T) -> T { return x }

Which brings up an interesting part of the design I'd love to hear people's thoughts about: overloading on Copyable.

What are the behaviors and constraints that a Copyable type cannot support when treated as though it is ~Copyable?

A move-only type is not required to have a deinit. So a value that supports copying, but isn't actually copied after being assigned to an any ~Copyable variable, isn't a problem. It will get a default value witness for destruction, just as if it were cast to Any.

3 Likes

Sorry, typo: I meant as I described in the paragraph aboveā€”where Self: Copyable. I did not mean to suggest that it is utterable, since S is non-generic; rather, I meant the notional application of that rule would dictate that this would be an error, and an extension would by that reasoning have to explicitly state Copyable conformance (or give up trying to conform). This would be because lack thereof would lead to an inferred, disallowed constraint (precisely because we'd be trying to extend a nongeneric type and a where clause cannot be applied).

This is, admittedly, severe for unconditionally noncopyable types that are just being extended to add a method or something, so probably unworkable.

My argument is that we should not special case this whatsoever; whether this is allowed without any diagnostic, gives a warning, or gives an error should fall out of rules we agree on for actual, non-silly usages.

What I was replying to was your argument earlier that a point against allowing extension S: ~Copyable was that if we allow it then we must also allow struct S: Copyable, ~Copyable, and that because the latter is silly it's a point against the former. Not so: I am saying that we ought to give no weight to this at all.

3 Likes

Copyable types also have deinits (that's how class references get released when a value goes away, for instance). You just can't write one explicitly (yet?).

5 Likes

Iā€™m not fully up-to-speed with the new ownership keywords, but perhaps what youā€™d be looking for is not a requirement that the type be fundamentally non-copyable, but rather that the specific value that is passed as an argument to your function be owned exclusively by your function for the duration of its execution, which I think would be spelled something like this:

func foo<T>(_ bar: consuming T) { }

Can someone confirm or correct my spelling?

P.S. I realize that it may be surprising that the signature above does not accept ā€œmove-only typesā€ (i.e. ~Copyable) despite the parameter having explicitly renounced its need to copy the argument. Would it make sense for the consuming keyword to imply ~Copyable under certain appropriate circumstances such as this?

Between the fact that 1) Iā€™ve never used the new ownership keywords, 2) Iā€™m not even 100% sure that consuming exists as such, and 3) Iā€™m not personally familiar with any of the real motivating use cases for ~Copyable, Iā€™m somewhat out of my depth on this issue and perhaps my critique is nonsensicalā€¦ but Iā€™m looking forward to finding outā€¦

Yep. Here's a thought experiment. In generics, the behavior of a type parameter T is defined entirely by things I know are true about T. For example, the only way that T can conform to Equatable is if this is a consequence of explicitly-stated requirements.

So the knowledge that the concrete replacement type for T definitely does not conform to some protocol (Copyable, or Equatable, it doesn't matter) doesn't actually give you any new information about T! From inside the body of a generic function, it only matters if I can always prove that T is Copyable. Otherwise, it doesn't matter if T may be Copyable, or if it definitely is not Copyable; neither possibility leads to any consequences for T.

10 Likes

I didn't see much discussion of availability in the proposal. Can you have a non-conditional conformance with partial availability? Surely that, at least, would require a Copyable conformance in an extension.

@available(dishwasherOS 1.0, *)
public struct SpinnerMotor: ~Copyable {
    // In dishwasherOS 1, SpinnerMotor was implemented 
    // with a non-copyable stored property...
}

@available(dishwasherOS 2.0, *)
extension SpinnerMotor: Copyable {
    // But in dishwasherOS 2, that was replaced with a copyable 
    // Int, so there's no reason new code can't copy it.
}
4 Likes

More than likely not. To properly support available Copyable conformances, we would need the ability to perform some kind of dynamic check before we could copy a value, and we probably shouldn't go there at all. We should revise the proposal to explicitly disallow this.

2 Likes

I'm having trouble following this. What are the circumstances in which the program would only copy the value conditionally at runtime? I imagine a conditionally available Copyable conformance working the same way any conditionally available conformance works. Availability checking would restrict use of the conformance to locations where it can be proven to be available. Any contexts in which the availability of the conformance is not proven would have to treat the value as non-copyable.

1 Like

consuming just means the receiving function takes ownership of the value, not that the value cannot be copied. They're largely orthogonal, I think?

I believe the value will in fact be copied if it's copyable and the caller still needs it - the callee receives only the copy (which it's given ownership of).

consuming or borrowing allow the use of non-copyable types. Which does make me wonder what the following is supposed to mean, because it seems invalid:

func foo<T: ~Copyable>(_ bar: T) { ā€¦ }

ā€¦since how is the ABI supposed to be determined for this, as to whether the argument is borrowed or consumed?

Hm, ok, well if this is true then I do think I'm missing some pieces of the conceptual puzzle. If we copy a value and pass the copy as an argument to a function, doesn't the function always "have ownership" of the copy? I thought the point of marking a parameter as consuming was that the value gets "used up" by the function and the callee is not allowed to "still need it".