Neither Copyable nor ~Copyable are types (or sets). There is no better way to conceptualize this than to say that Copyable is a protocol, while ~Copyable is a special syntax to disable the implicit conformance requirement to Copyable that can appear in certain positions. There's no deeper "meaning".
The truth is that ~ doesn't mean anything on its own in type context. ~Copyable (and ~Escapable) cannot be understood by decomposing them further.
Since Bar is a concrete non-generic type, the moment we declare it to be ~Copyable in its definition, the "copyability" becomes settled and there'll be no way to extend it to be Copyable. The compiler will reject any attempt to make it conform to Copyable using an extension.
I guess this is confusing some folks like past me who would like to parse ~Copyable as a prefix operator ~ + Copyable. No one would be confused if ~Copyable were other spelling such as #disableImplicitConformance(to: Copyable). I don't want to write such a long keyword though.
Imagine a Swift dialect identical to real Swift in every way, except without default conformances to Copyable everywhere. It would be possible to write a preprocessor from real Swift to this Swift to insert Copyable everywhere except where ~Copyable appears. From this point of view, T: ~Copyable means no requirements at all in the âtrueâ Swift. Just like you can pass a type that happens to be Equatable to a generic function that doesnât require equatability, you can pass a Copyable type to something that doesnât require Copyable. The additional capability is just not used, like any other protocol conformance.
This way of looking at it is consistent with the nominal type case, because struct Foo: ~Copyable really means âempty list of conformed protocolsâ, so Foo does not conform to Copyable. Otherwise it does.
Coming from a perspective of having to teach programming to complete beginners, I see it like this: ~Copyable points to the fact that actually the meaning of T: is more complex than it seems.
When you define a concrete typeT, putting that : behind it defines it as having a certain behavior, expressed by a protocol or a superclass (if T is going to be a class and should inherit from another). This is already a double meaning (as inheriting from a superclass is not exactly the same thing as conforming to a protocol). The more important thing, however, is this:
When you refer to any type by using a placeholder T, i.e. T is a type parameter in any form of generic code, putting that : behind it does not "give" the type behavior, it describes a requirement for any type that can be used in that parameter.[1] This is fundamentally different and I have seen newbies having difficulties understanding that. This lack of understanding is exacerbated by the fact that the distinction usually doesn't matter. Before we had the ~ stuff, it didn't really matter whether you thought of that : as "giving" a type behavior or expressing a requirement, the end was that whatever you did with T, you could treat it as if it "has" the behavior.
Now, the ~Copyable makes this more fatal when talking about the type parameter aspect. In terms of the requirement the semantics become more tricky. "Any T used here is not required to be Copyable", is a different statement than "Any T used here is not Copyable".
When talking about defining a concrete type, where you "read" the : differently, you simply don't have the problem. Here the : means "it has this behavior", so "it is not Copyable".
It also helps a lot to remember that ~Copyableremoves the protocol Copyable from the implicit list of conformances for a type you define or from the implicit list of requirements a type parameter has. If you read the : like I described above, it makes things easier.
I am using "type parameter" a bit loosely here, the same thing applies when defining a new protocol that inherits from an existing one âŠď¸
It's always interesting to hear the perspective of folks teaching Swift.
While I wouldn't teach this to beginners, something that might help clarify your own mental model further is the fact that the left-hand side of a conformance requirement in a where clause need not be a type parameter.
We allow this:
struct G<T> where Array<T>: Equatable {}
What does it mean? Well, there is actually a pretty logical interpretation. It's true that Array<T> is Equatable if and only if T is Equatable, because of how the conformance was declared in the standard library, so indeed, the above is equivalent to this:
struct G<T> where T: Equatable {}
You can even do this:
struct G<T> where Int: Equatable {}
This looks absurd, but Int: Equatable is trivially true as a requirement, and its truth doesn't depend on T at all. So it's the same as this:
struct G<T> {}
On the other hand, this where clause can never be satisfied, no matter the T:
struct G<T> where Int: Sequence {}
The error message error: type 'Int' in conformance requirement does not refer to a generic parameter or associated type is actually slightly misleading, but in a good way: in most cases, you made a mistake and meant to refer to a type parameter on the left, and you're not actually trying to state a tautological requirement involving a fully concrete type.
Yeah, this is probably how I would approach explaining the feature. We don't want to teach most features in terms of what is essentially their desugaring to a simpler core language, but in the case of ~Copyable I think this is actually the best way to go about it.
I should probably add that I of course don't blast my students with this on day one. Also, it's (mostly) not structured lectures I talk about, but rather one-on-one discussions and teaching sessions I had with trainees and younger colleagues that made me realize where pitfalls in understanding the syntax can be. So my n is probably not statistically significant, it's just personal anecdotes.
It's interesting, by the way, that where clauses are so close to traditional mathematical logic. I mean, it makes sense, but obviously I never tried to add a trivially true statement in there as that has no real usefulness (at least I cannot come up with one for where Int: Equatable). Guess in the future I might refer to this when explaining it to my students (if they have a CS background and had a class on this).
I think that part of the problem today is that struct Foo: ~Copyable {} is adding features that aren't available without : ~Copyable and this breaks the usual explanation that Copyable is like a protocol and ~Copyable is simply removing the conformance to it.
Specifically, struct Foo: ~Copyable {} can have a custom deinit where as struct Foo: {} can't.
struct Foo1: ~Copyable {
// compiles fine
deinit { print("deinit") }
}
struct Foo2 {
// error: Deinitializer cannot be declared in struct 'Foo2' that conforms to 'Copyable'
deinit { print("deinit") }
}
borrowing switch is similary only possible without Copyable conformance today:
// compiles fine
extension Optional where Wrapped: ~Copyable {
public borrowing func borrowingMap<U: ~Copyable, E: Error>(
_ transform: (borrowing Wrapped) throws(E) -> U
) throws(E) -> U? {
switch self {
case .some(let y):
return .some(try transform(y))
case .none:
return .none
}
}
}
// without Wrapped: ~Copyable
extension Optional {
// error: 'self' is borrowed and cannot be consumed
public borrowing func borrowingMap<U: ~Copyable, E: Error>(
_ transform: (borrowing Wrapped) throws(E) -> U
) throws(E) -> U? {
// consumed here
switch self {
case .some(let y):
return .some(try transform(y))
case .none:
return .none
}
}
}
The later might just be a bug but the former seems like a restriction that is put in place on purpose.
This doesn't have to be like that though. Structs have a secret deinit and swift could make that customizable, similarly to how it could make a copy customizable which togehter would be useful for a partial initialized version of InlineArray and much more.
I think you can look at this as a more severe instance of a general principle: Conforming to a protocol grants more capabilities to clients of the type, but puts more obligations on the implementer. In the case of Copyable, those obligations are more intrinsic than only needing to implement a few methods.
As an aside, I really wish diagnostics would differentiate between implicit and explicit protocol conformances (someone seeing this for the first time won't know where this Copyable protocol is coming from). Even better, the diagnostic should point to the protocol conformance so we can see any inline docs available, or if there's a custom implementation.
I admit to not having read this entire thread. Here's my disorganized thoughts on the matter:
If I were designing Swift from scratch (with the benefit of hindsight!), I would make deinit available on value types with the caveat that if you implement it, you must also implement another magic member function called copy. I know the core team does not universally agree with this idea; if you think it smells a lot like C++, that's not accidental. (Heck, if it were up to me, refcounting would be implemented atop this scheme and reference types would be visible in source as RefCounted<T> or some syntactic sugar for it like T^ in C#⌠maybe we shouldn't pay too much mind to my opinions here?)
I think if we had an explicit copy member, then ~Copyable would make more sense since it would allow you to say "I have a value type with a deinit, but no copy member". Right now since copies are entirely transparent to the developer, it can be non-obvious when or where they might occur, or that they even occur at all. If I, a novice developer, don't know what it means to copy a value in Swift, I probably won't understand what ~Copyable does or why it might be useful.
Again with the benefit of hindsight: perhaps we should not have used ~ as the sigil for protocol suppression. Perhaps it should have been spelled Copyable? or maybe Copyable instead. (Yes, I know the former conflicts with optionals. I'm not trying to relitigate here, just wondering whether a different spelling would make the meaning clearer.)