[Pitch] Noncopyable (or "move-only") structs and enums

Was consideration given to using NonCopyable as a marker protocol, rather than @noncopyable as an attribute? I didn't see it discussed in the "Alternatives Considered". It seems like Swift has recently preferred to express such features with protocols, rather than attributes. (For example, see Sendable.)

6 Likes

I think this is implicitly answered by:

NonCopyable is not a constraint, it is the lack of a Copyable constraint. It doesn't make sense to think of it like a protocol in any other context, since a generic parameter or protocol that allows for noncopyable types to conform would not require the type to be noncopyable, it allows the type to be noncopyable, and would still accept copyable types.

It could make sense to adopt a "don't require this constraint" syntax like Rust's ?Trait, so you'd write struct Foo: ?Copyable or something like that to opt the type out of satisfying the Copyable constraint. Then eventually you'd also be able to write foo<T: ?Copyable> to specify a generic parameter that doesn't have to be copyable and so on. "Copyable" as a constraint still doesn't quite make sense to think of as a protocol, though, because it's a fundamental trait of the type, and can't be retroactively added or have multiple independent conformances like a protocol can, and it's not a "marker protocol" either because it has a fundamental runtime ABI impact on the type. I think it would end up being more akin to a layout constraint like AnyObject, which is also implicitly "conformed to" by all types that satisfy its requirements (that is, single-refcounted-pointer types like classes and class existentials without witness table requirements), and cannot be explicitly or retroactively conformed to by types that don't fundamentally satisfy that constraint.

4 Likes

Is “Unique” is an accurate synonym for “NonCopyable”? That’s a positive constraint.

I also don’t understand the conclusion that copyable types would always satisfy NonCopyable type parameters. struct T<U: NonCopyable> doesn’t allow any copyable types to substitute for U.

There isn't any fundamental that prevents a copyable type from being used as a noncopyable one. The implementation just wouldn't use the copy functionality. It's like talking about "types that don't have a foo() method" when you have a protocol Foo { func foo() }. We could make up a different requirement for "must-be-unique", but we'd have to decide what the important properties of types that satisfy that requirement are, and what sorts of code you'd write that takes advantage of those properties. It still wouldn't be something that noncopyable types just automatically satisfy, because nothing prevents a noncopyable type from implementing some manual-copy API, or being used to represent a handle to a shared resource that the client is not allowed to propagate but might have handles elsewhere in the program or OS.

1 Like

That’s precisely what I would want the @noncopyable attribute to do: prevent var x = MyNonCopyable(); let y = x. Generalized, this is func withHandle<Handle: NonCopyable, T>(_ handle: borrowing Handle, perform closure: (borrowing Handle) rethrows -> T) { closure(handle) }. Another name for withHandle could be ensuringUniquelyOwned.

I presume MyNonCopyable() is concretely a non-copyable type, so let y = x would in fact move x into y rather than copy it. When we generalize the generics system to include noncopyable types, then withHandle can't assume Handle is copyable, and it would act as a noncopyable type within the body of the function. So the function would not be able to introduce copies of whatever Handle is, but callers can still pass in copyable types for the Handle that they can copy themselves.

You may also be interested in the @noImplicitCopy attribute, which we're implementing to suppress implicit copying on values that are of copyable type.

1 Like

It is useful to be able to write generic code that works on an arbitrary type, copyable or not. Many basic data structures and algorithms do not innately require copying values and so can naturally be generalized to work with non-copyable types. For correctness, code like that has to work generically with the type as if it were non-copyable. When given a copyable type, it effectively promises not to (locally) copy it.

From that perspective, copyability is a positive constraint, just like an ordinary protocol constraint. For example, if Array were generalized to allow non-copyable types, it would still be conditionally copyable:

extension Array: Copyable where Element: Copyable

(You might think that you could do the same with NonCopyable as a positive constraint, and in simple cases this does make sense:

extension Array: NonCopyable where Element: NonCopyable

But the basic direction of the logic is wrong, as you can see when you consider a similar generalization of Dictionary:

extension Dictionary: NonCopyable where Key: NonCopyable, Value: NonCopyable

This is wrong; it is making Dictionary non-copyable if both of the key and value types are non-copyable, but in fact Dictionary needs to be non-copyable if either argument is non-copyable. Similar logic applies to e.g. inferring non-copyability for structs. This is a clear sign that the direction of the constraint is backwards.)

13 Likes

How does the compiler know that it’s OK to pass a @noncopyable type to such a function? Just because a parameter is marked borrows doesn’t mean the function can’t copy it internally. There must be some other element of the function’s type signature that the compiler can check at the call site, no?

That's the part of the proposal we're punting to another day by saying "you can't do that" at all. But yeah, in the fullness of time, there will need to be some way to indicate generic parameters that don't require their types to be copyable. A few ways we've thought about doing this are to have a Rust-style T: ?Copyable syntax to say that T does not require Copyable, or a declaration level @allowNonCopyable that specifies that generic parameters don't implicitly require copyability, so you can then explicitly put T: Copyable on the ones that do require copying.

2 Likes

Without this, how can one actually do anything useful with a @noncopyable type? The pitch talks about borrowing/inout/consume, but none of these parameter decorations imply the callee won’t try to copy the argument. It seems like solving that problem is a prerequisite, not something that can be deferred.

The callee won't try to copy arguments that are concretely of noncopyable type, because it can't; there is no copy operation anyone can use on the type. The impacts this has on behavior inside the function are pretty much identical to those that apply to @noImplicitCopy bindings: if you have a borrow parameter, you can only do borrow things with it and can't consume it; if you have an inout parameter, you can borrow or modify the parameter, but have to give it back when you're done; if you have a consume parameter, it's yours to do with as you please, up until you give it someone else to consume.

Ah, this explains why @noncopyable cannot be applied to classes—otherwise a value of @noncopyable class Sub : Super could be passed to a parameter of type Super, and the callee could would try to copy it. The problem is any kind of polymorphism, not just generics.

One big difference is that @noImplicitCopy is documented to be ABI-neutral, whereas it would be an ABI break to remove the marker for “generic over copyable and non-copyable types”. So there would be another keyword or syntax which does almost the same thing as @noImplicitCopy.

One of the good changes that happened during the design process for concurrency checking is the change from @Sendable-everywhere to : Sendable. It reuses existing language concepts, making sendability feel more integrated with the standard library and the language. By contrast, all these variations, with and without @ prefixes, are reminiscent of Java annotations and make move semantics feel more like a bolt-on feature.

I'm not a great fan of the @noImplicitCopy naming or syntax, and would definitely love to come up with something better. The reason @noImplicitCopy doesn't affect ABI is that it's an artificial local constraint, that ties the compiler's hands from using its normal ARC and value semantics tricks to manage memory for a particular value, whereas whether a type even has a "copy" operation at all to stick in the value witness table is a fundamental ABI concern. I think we will however want to be able to allow some degree of retrofitting noncopyable type support into the ABI; the basic calling conventions for passing a noncopyable value don't change, and it would be useful to be able to replace standard library APIs with implementations that don't rely on copying, and then expose them as not requiring copyability in a new version of the OS.

The "bolted-on"-ness is maybe a little bit intentional, since we still expect a good majority of Swift code will remain oblivious to copy controls and still be fine with automatic memory management, so we're trying to keep noncopyable types and the various controls on copy behavior on the "progressive" side of "progressive disclosure". I like the idea of a Rust-style : ?Copyable constraint remover; that seems like a nice unifying syntax for opting types out of providing copyability and stopping generics from requiring it. Is var x: ?Copyable = copyableValue() too weird as a way of spelling @noImplicitCopy on an individual binding?

2 Likes

My concern is that code which needs to use this feature will be surrounded by other code that needs to use this feature; e.g. the core of an application’s hot loop. Will such pockets of code by peppered throughout with @nonmoving and @noImplicitCopy?

Although we only currently have it wired up for variable bindings, I think it'd also be useful to have @noImplicitCopy scopes, which wouldn't completely eliminate the markup, but could mean that you have to only write @noImplicitCopy do { } once around your hot loop.

3 Likes

I read through the pitch but its not clear to me if there's any way to mark a type as not being deinit-able. Example use: say I had type like AsyncInterruptTrigger which releases a suspension point elsewhere. I the library author want to enforce that the owner of one of these triggers either calls fire() or cancel() in order to consume the instance. How would you do that with this pitch?

EDIT: I realized this was discussed earlier as "linear vs affine types". I still think this is something worth mentioning in the pitch under alternatives considered or future directions or both.


I know this isn't being pitched, but do you see a future direction where a language mode could be applied to entire module to opt into this behavior? I see use in environments such as firmware HAL libraries where you might want to flip the defaults?


English language bikeshedding

Some dictionaries specify that "copiable" is the standard spelling for "able to copy", although the Oxford English Dictionary and Merriam-Webster both also list "copyable" as an accepted alternative. We prefer the more regular "copyable" spelling.

The negation could just as well be spelled @uncopyable instead of @noncopyable. Swift has precedent for favoring non- in modifiers, including @nonescaping parameters and and nonisolated actor members, so we choose to follow that precedent.

Others have mentioned the use of an @attribute feels a bit off. I think this concept is more simple to explain syntactically to new users by leveraging existing precedent set with Sendable:

struct FileDescriptor { }

@available(*, unavailable)
extension FileDescriptor: Copyable { }

I think this has value if a language mode to switch the default is in the cards as making a type Copyable can also leverage the same familiar syntax.

// My module with some language mode flag like --disable-implicit-copyable
struct Foo { }

extension Foo: Copyable { }
1 Like

Another possibility to reduce annotation noise might be for us to say that types implicitly lose their copyability when they do something that forces them to be noncopyable, like declaring a deinit or containing a noncopyable field. We already do this with Sendable for non-public types, which are implicitly Sendable if their components all are.

1 Like

We're also discussing a @noImplicitCopy attribute that can be applied to normally-copyable things to get fully manual copying behavior. Whether a type is copyable or not is a fundamental part of its representation, so it wouldn't work well to have types sometimes be copyable or not, so I think that sort of annotation, to tell the language not to implicitly take advantage of copying even if a copy operation is available, is a better way to go for the sort of situation you describe. The pitch that's up only discusses @noImplicitCopy as a variable modifier, but I think we could support it for scopes as well, and one of those scopes could be "the entire module". However, one thing to work out with these scoped noImplicitCopy modifiers is whether there should be exceptions to allow some types to always be copyable. You probably never really care if an Int gets implicitly copied, for instance, and it would be annoying to have to think about copying at that level.

1 Like

I think this is really the right way to go. I'd expect it to be the common case that we care about copyability not just for a specific type but for regions of code or entire modules. I'd argue it's less ergonomic to have to express negative constraints repeatedly for individual arguments or types than to flip the defaults for regions of code (files, modules, or possibly sections of files using something like #pragma); I think it'll be rare to want to think explicitly about copyability for one type or parameter but not for all within a scope.

I think it would make most sense to treat implicit conformance as a module-level attribute that's turned on by default, and that if you passed e.g. --disable-implicit-copyability it would flip the default for that module. You could then re-enable it for sections of code that you haven't migrated or don't intend to migrate using something like @beginScope(ImplicitlyCopyable).

Under this sort of model, you don't need to consider negative constraints; you would write the code in a scope where the Copyable constraint is assumed not to exist.

In this design, all existing modules would be treated such that all types automatically conform to Copyable, all generic constraints have an implicit Copyable constraint, and all protocols descend from Copyable. In a scope that opted out of implicit copyability, those extra requirements would have to be spelled out explicitly, and all types and variables within that scope would need to be explicitly copied (assuming they conform to Copyable) or moved.

With regards to types that we still want to allow implicit copying on: a module built without implicit copyability enabled could introduce its own conformances to a marker protocol ImplicitlyCopyable for Copyable types (e.g. extension Int: ImplicitlyCopyable {} if Int was not marked as ImplicitlyCopyable in its defining module). It could also conform any types it vends to ImplicitlyCopyable, which would also imply a Copyable conformance. Any structs or enums that conform to Copyable would not be allowed to have a deinit.

As a side note: we could apply this same solution to Opt-in Reflection Metadata, since again that's a place where we're wanting to change the default for regions of code – in that case implicit conformance to Reflectable rather than Copyable.