[Second review] SE-0390: Noncopyable structs and enums

It seems to me like it would be very helpful/relevant to discuss more explicitly the other possible/expected uses of this operator (or keyword). These imagined other usages have been referenced in this thread more often than they have been explicitly described.

This was one I was surprised by:

I didn't know that bare enums get Equatable and Hashable for free, but that seems weird to me. Is there a good reason it's like that? If not, is there any talk of changing that in Swift 6?

Aside from ~Equatable for bare enums (which could be solved instead by removing the implicit conformance for bare enums), are there any other actual use cases for this operator?

Implicit Sendable is the other significant one. This is described in more detail in the proposal's future directions.

This notion of a "layout constraint" as distinct from a protocol is one that I haven't had to deal with much yet.

@Joe_Groff's explanation here leaves me with at least one concrete question:

Am I correct in believing that the below code is non-sensical?:

class SomehowNotAnObject: ~AnyObject { }

Yeah, it (at least currently) doesn't make any sense for a class type to not satisfy AnyObject, since that's part of the intrinsic type requirement to be AnyObject. (One possible future direction arising from C++ and other platform interop projects could be classes that have custom refcounting behavior, not compatible with the system retain/release implementation used by AnyObject, and those kinds of classes might not satisfy AnyObject at that point.)


Couldn’t we model Copyable as a protocol with a copy requirement that borrows self to return another instance of Self? A type containing non Copyable instances should (in the long term) be able to conform to the Copyable constraint by offering a custom copy implementation. That’s why I also recommended a more general protocol for explicit copying upthread:

With regards to the name, I didn’t see a lot of discussion about the following point.

It makes a lot of sense to make the drop-implicit-conformance syntax part of the constraint operator rather than the protocol. That is, the protocol expresses a constraint, but the syntax in question just removes implicit conformances to this constraint. However, a -: style syntax would make it more difficult to write existentials or constraints in the some Protocol syntax.

1 Like

Yeah, I fully agree. Of all the options I've seen so far, I like @benlings's without Copyable the best, since you could still easily spell that as a constraint down the road.


i think if we are having such difficulty agreeing on what single-character symbol (?, -, !, ~) this should use, then that is an indication that we should not denote this with an easily-misunderstood symbol, and use a more verbose spelling like without instead.


I agree. I started the second review with a single-character sigil because I couldn't think of a good word that had the right connotation, but without does a great job. The only place I can think of that reads kind of awkward is any without Copyable as the syntax for an existential that may contain noncopyable values, but that's not the most important use case.


how about unknown Copyable?

1 Like

In practice, a copy implementation pretty much always needs a matching destroy operation to clean up the resources used by the new copy, and that destroy implementation has to be a fundamental traits because it's what happens implicitly when a value's lifetime ends, so by extension, I think the copy operation is best treated as a fundamental type trait as well.


Right, I hadn’t considered this. What is entailed by the destroy operation that makes it unsuitable as a protocol requirement?

In languages like Typescript unknown has a meaning akin to Swift’s some Any. So unknown may cause some confusion, especially for users of other languages, as it would be interpreted as a type (contrary to a constraint). However, followed by a protocol, its function would be quite evident and it’s pretty concise, so +1 from me.

1 Like

General question about noncopyable types:

Can methods on copyable (“normal”) types be marked consuming? Or is that only valid if the type is without Copyable?

Protocol conformances are separate and extrinsic to the core type definition, since they can be added independently by extensions, and you can even have more than one conformance if two modules extend the type to conform to the same protocol. When a value goes out of scope and the compiler needs to know how to destroy it, there should really be one definitive answer as to what to do; we don't want the implicit destruction behavior to depend on what modules happen to be in scope, whether an extension matched the type in a different file, or some other action at a distance.

In the first review, people brought up the possibility of "must consume" types, which aren't allowed to be implicitly destroyed and must be explicitly passed to a consuming method to end their lifetimes. One way to model that might be to treat Destroyable as an implicit type trait, like we're making Copyable be with this proposal, so a type that requires explicit consumption could be declared struct Foo: ~Destroyable (or struct Foo: without Destroyable). But I think, in that world, we'd still want Destroyable to be an intrinsic layout constraint that's tied to the core type definition.


Yeah, the consuming and borrowing modifiers can be used on methods and parameters of copyable type too. For copyable types, these modifiers don't have as much impact on caller behavior, since the compiler can copy as necessary to maintain the apparent lifetime of values, and is more of an optimization. If a method implementation can consume a copy of one of its parameters, then it's usually more efficient to pass the argument as consuming, so that the value can be forwarded directly to its eventual new owner without needing to copy it to keep the original alive.

Just wonder. When creating a type, the type by default conforms to Copyable.
So, could we override the default Copyable’s implementation?

Yes, it wouldn’t make sense to model Copyable as an everyday protocol. The main limitation, though, is that layout constraints must be declared on the same file as the type definition. So, whether we call the resulting construct a protocol or not, it would make sense to be able to override the default copy() implementation. If we stick to the (pseudo-)protocol analogy, Copyable conformance is automatically synthesized by copying all fields of a type independently; by writing a copy() function ourselves, we prevent this synthesis and provide our own implementation. Of course, where we need a custom copy() we probably also need a custom destroy().

Explicit destroys would be especially useful for situations where cleanup is expensive. I think both custom copy/destroy and explicit destroy are natural extensions to customizability, and should be added as future directions.

My (shallow but very strong) argument against ~ is that the character is all over the place on various keyboard layouts and the language shouldn't imply everybody uses a US keyboard/layout or one single layout, even though it may make the most sense as ~ is used to express "approximately", so "maybe copyable". Actually, on my keyboard, left of 1 is not ~ but ±, which come to think of it, could mean "maybe". If and only if I had to choose one single character prefix, I'd choose minus. I'm still not convinced ! is a bad idea. And, half jokingly, how about Copyable-- :smiley:

Edit: I'm saying if and only if just because I would prefer explicit wording preceding Copyable like others have proposed


Huh? I thought just upthread that was called out as impossible. Is it an error to have multiple conformances only if those are defined in the same modules?

It may behoove us to consider identifying the “protocols but not really” protocols in some way so they can be reasoned about clearly. Obviously it’s outside the scope of this discussion, but some way to identify that a protocol is a marker (e.g. marker protocol) or specifically for layout (e.g. layout protocol) would be something to discuss.

Could we just remove the implied conformance by providing a copy() conditionally and guarantee the lack of conformance by providing a copy() that returns Never to explicitly say that copy can never happen?