SE-0427: Noncopyable Generics

That is well put: we can consider that an explicit ~Copyable requires not requiring Copyable conformance.

Just as metatypes are a more advanced concept than plain types, such “meta-requirement” semantics are a step beyond plain generic requirements.

I am not sure making it this meta gets us incrementally much more than sticking to “maybe” semantics and ensuring diagnostics point out manifest silliness. But it is a nice formulation!

We still need to be careful, though, not to conflate “requires not requiring conforming to Copyable” with “requires not conforming to Copyable”—and I am wary of anything that causes ~Copyable to behave as-if the latter.

3 Likes

Yes, I think this is the (subtle) distinction that @kavon was getting at originally:

I think in order to have a solid user model of the "maybe"/suppression semantics it is fundamentally important that this diagnostic be in place and not just treated as a convenience detail. If treated just as a convenience, a weaker (insufficient) version of the rule might prohibit only straightforward syntactic conflicts where Copyable and ~Copyable both appear in source as in, e.g., Copyable & ~Copyable. But then transitive constraints could still reintroduce a Copyable constraint, and ~Copyable could at best be read as "maybe allowed to be non-copyable, but maybe required to be copyable" which much less useful than the "maybe" reading we'd want, that is, "maybe copyable but permitted to be non-copyable".

1 Like

T is only "maybe" Copyable because any requirement for it to be Copyable is absent. The type T here inside of identity is also "maybe" Equatable, Hashable, SetAlgebra, and every protocol under the sun, because no requirement exists for those protocols either, yet types that do support those capabilities can still be substituted for T / passed into identity.

So I think both ways of thinking about it are fine and can work coherently. The only situation I can think of is that struct S: ~Copyable {} definitely does not conform to Copyable, rather than "maybe".

I personally don't like using "maybe" because it feels rather wishy-washy and can be said about every protocol. Using "absence" leaves no ambiguity, but does force you to think a tiny bit about the consequence of removing a requirement.

2 Likes

Would it be helpful to describe a T: ~Copyable constraint as ‘the list requirements on T doesn’t include Copyable’? I think this makes it clear why it should be an error for some other requirement to include Copyable.

1 Like

I can certainly get on board with the "meta-requirement" semantics here. I do think the distinction is of huge import, though. Put another way, this would be consistent with (one possible reading of) @kavon's revised formulation:

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" .

...but not (as you quote replied) his original formulation:

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 required to not be Copyable" .

3 Likes

Yeah struct S: ~Copyable {} is the case that I think necessitates thinking in terms of "suppress Copyable" or "absent Copyable" if one really wants to have One True Reading of ~Copyable. But then, as you note, the downstream consequences of Copyable being suppressed can differ depending on what sort of declaration ~Copyable is applied to, and I think we will necessarily have to teach those cases separately. As this thread evidences, it is not immediately intuitive what the effects of suppressing Copyable are for each use case, and I don't think it's wrong for users to have a 'piecewise' understanding of ~Copyable such as:

  • In struct S: ~Copyable it means that S cannot be copied
  • In struct S<T: ~Copyable>: ~Copyable it means that:
    • Substitutions for T are not required to be copyable (but "maybe" any given one is)
    • S is not copyable by default (but "maybe" has Copyable reintroduced conditionally)
  • In protocol P: ~Copyable it means that types conforming to P are not required to be Copyable (but "maybe" any given conformer is in fact Copyable)

Of course, IMO the generalization is still immensely valuable (and potentially elucidating), especially as an explanation of "why do each of these positions use the same spelling?", uncovering the fact that T: ~Copyable behaves internally just as S: ~Copyable, etc.

4 Likes

I completely agree. The formal model relies on suppressions to create the absence of Copyable, and I see no harm in people’s intuition preferring a slightly leaky abstraction on top of that.

1 Like

So there is going to be a problem with getting people to understand what <T:~Copyable> does by saying "Working with Copyable is just like working with other protocols"! because one doesn't need doesn't need <T: ~Equatable, ~Hashable, ~SetAlgebra, ~EveryProtocolUnderTheSun> and the typical Swift user will have been intentionally sheltered from being aware of the implicit :Copyable addition from what I understand.

I know progressive disclosure is important, but I hope the documentation for structs and enums puts up front something like "A struct/enum is a type with an implicit conformance to the following special protocols (Copyable, Escapable, etc.) Special protocols are different from regular protocols in that..." to make clear why ~SpecialProtocol is needed but ~Hashable isn't and would never be with this current usage of ~

ETA: This part right here is no longer actually true. (Documentation)

"All structures and enumerations are value types in Swift. This means that any structure and enumeration instances you create — and any value types they have as properties — are always copied when they’re passed around in your code."

Maybe better?

"All structures and enumerations are value types in Swift. This means that any structure and enumeration instances you create — and any value types they have as properties — are copied when they’re passed around in your code through their conformance to a special protocol, Copiable. The typical programmer will not have to interact with this protocol except under very special circumstances. "

1 Like

I want to point out something stupid that should still be valid:

struct X: ~Copyable {}
extension X: Copyable {}

With this code, X is Copyable. This demonstrates that ~Copyable does not mean "unconditionally non-copyable" when applied to a non-generic type. It really does mean "suppress Copyable" or "don't include an implicit conformance to Copyable" here.

This example is silly when written like this, but I could see it being reasonable for macro-generated declarations* that work on both generic and non-generic types. (Rust has run into that kind of thing, though their macros are much less constrained than Swift's are.)

* when macros support generating declarations and extensions at the same time, can't remember if they do yet

9 Likes

Maybe this should be valid:

struct G<T: ~Copyable> {}
extension G: Copyable where T: Copyable {}

I see no technical reason to demand that the conformance on the type is suppressed if the user then declares an extension, since both must be in the same module anyway; it’s mostly a matter of style if we allow one or both, and ban the other, etc.

1 Like

I worry a little about that if the extension is in a different file, or even several pages down in the same file. It's no trouble for the compiler, but is it better or worse for humans? :person_shrugging:

4 Likes

As a general rule I’d also prefer to start out on the side of requiring (perhaps excessive) explicitness that could be relaxed down the road by introducing additional inference rules rather than preemptively introduce conveniences that would be difficult to roll back (if we later think they were a mistake) and/or potentially constrain future evolution.

4 Likes

Sure, documenting this in the Swift book is definitely something that should happen.

I want to point out that we also automatically try to infer implicit conformances to Sendable for non-public nominal types. In addition, enums that only have cases with no associated values, i.e., enum E {case yellow; case blue} automatically get implicit Hashable and Equatable conformances. If someone wants to optimize every ounce of code size, they'd find ~Hashable useful to suppress the implicit conformance of an enum to Hashable in its inheritance clause.

I don't see any value in allowing you to write this, though:

protocol Q: ... {}
func f<T>(_ t: T) 
  where T: Q, T: ~Hashable {}

There's no implicit requirement T: Hashable that we'd be suppressing here, so this code basically is just asserting is that Q doesn't require Hashable. That's something I think an IDE should be responsible for telling the user if they are curious.

I think people will also want a less clunky way to be able to suppress the inference of unconditional Sendable conformance on a type. My question is, do they also want to prevent that unconditional conformance from being introduced in an extension? Even an extension in the same file? If ~ really only means suppress and not prevent, then we can't quite do away with this:

@available(*, unavailable)
extension X: Sendable {}

Because this extension actually does both suppression and prevention, but only of the unconditional Sendable conformance. You can still also add this if S is generic:

extension X: Sendable where T: Sendable {}

Personally, I think there's a lot of peace-of-mind with having both suppression and prevention that ~ can give you. It also does away with the need for this unavailable-extension trick.

2 Likes

Thank you that was help a helpful reminder about those having background conformances, that it won't just be Copiable and Escapable as possible ~-ables.

This seems like a remaining thing that I'm seeing some inconsistent messaging on. Whether ~ will suppress implicit only or suppress implicit AND prevent explicit, and I can see pros and cons to each!

(taking off my review manager hat)

No. For Sendable, it should be completely valid to explicitly say that a superclass does not have a Sendable conformance using ~Sendable, but add Sendable conformances to subclasses. Right now, folks have tried to do this with an unavailable Sendable conformance on the superclass, but that fundmentally does not work because the subclass inherits the unavailable conformance, meaning adding the @unchecked Sendable conformance on the subclass does nothing, leading to some very surprising behavior (the compiler should also warn you if you try to do this).

2 Likes

This doesn't make sense to me. Why would the presence of an unrelated generic cause the behaviour of ~Copyable to change in this position? Either way it should merely be saying "don't automatically make S Copyable", no?

Is this any different to any other protocol conformance via an extension, though? Special-casing Copyable in this respect would make the overall system more complicated.

Isn't that how you would express the limitation "I don't want to use Hashable on T", to have the compiler ensure you don't inside f?

That seems like a reasonable thing to do, in principle. I don't know how often it would come up, but then we've never had this ~ syntax before, so we have no experience with it yet.

It doesn't change the meaning, it's just that there's no way to spell the conditional conformance without a generic parameter. Either way ~Copyable just means suppress the implicit conformance.

Maybe I’m not fully following, but I don’t think that’s true. ~Copyable on a concrete type means ‘suppress the implicit conformance’. ~Copyable as a generic constraint means ‘accept (but don’t require) concrete types that have had their implicit conformance suppressed.’ I’d argue that’s a meaningful distinction.

1 Like

These amount to the same thing, so no, this is not a distinction. Or rather, it's a distinction about how expressing conformances vs constraining generic arguments are different things in Swift, not a distinction about ~Copyable:

  1. struct S: Printable means add a conformance Printable to S
  2. struct S: ~Copyable means suppress the automatic conformance of Copyable to S
  3. func f<T: Printable(t: T) means allow the body to use Printable on T
  4. func f<T: ~Copyable>(t: T) means suppress the automatic ability for the body to use Copyable on T

edit: and to take this further – this ends up resulting in:

  • func f<T: Printable(t: T) accepts (but doesn't require) things that are Squishable as an argument
  • func f<T: ~Copyable>(t: T) accepts (but doesn't require) things that are Copyable as an argument

This just falls out from 3. and 4. above rather than being a new rule.

4 Likes

Are we sure? Because @Jumhyn said it means:

It might seem pedantic but I think this is pivotal behaviour, at least insofar as how it heavily defines the conceptual model here.

If S cannot be made Copyable even by an extension, then that is completely novel behaviour in the language (and IMO a much more complicated protocol conformance model than what we have now).

1 Like