SE-0390: @noncopyable structs and enums

Hello Swift community,

The review of SE-0390: "@noncopyable structs and enums" begins now and runs through March 7, 2023.

This feature is already partially implemented on the main branch of Swift behind the -enable-experimental-move-only flag, available in the nightly main toolchains dated Feb 23 and later.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you,

—Stephen Canon
Review Manager

24 Likes
  • What is your evaluation of the proposal?
    My opinion is unchanged from that of the pitch thread:
  • Is the problem being addressed significant enough to warrant a change to Swift?
    Yes
  • Does this proposal fit well with the feel and direction of Swift?
    Yes
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    I believe this compares favorably to Rust's Copy & Clone traits, while fitting well with Swift
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Read the pitch and skimmed the final proposal

We could conversely borrow another idea from Rust, which uses the syntax ?Trait to declare that a normally implicit trait is not required by a generic declaration, or not satisfied by a concrete type. So in Swift we might write:

struct Foo: ?Copyable {
}

Copyable is currently the only such implicit constraint we are considering, so the @noncopyable attribute is appealing as a specific solution to address this case. If we were to consider making other requirements implicit (perhaps Sendable in some situations?) then a more general opt-out syntax would be called for.

Besides Copyable and Sendable, this could also be used for Hashable on enums:

enum MyEnum: ?Hashable {
    case dontHashMe
}
1 Like

I am generally in favor of the feature, including the choice to call it noncopyable instead of moveonly not to model it as a protocol.

However, as I expressed in the attached macro review, I am against using an @-prefixed attribute. It feels strange that an @attribute affects whether or not it is legal to include a deinit in a struct or enum declaration. It blurs the line between core language semantics and helpful annotations.

indirect enum doesn’t use an @. postfix func doesn’t use an @. Why should @noncopyable enum use an @? The @ can even be seen to imply a lack of confidence on the proposal authors to commit a true keyword to this model.

13 Likes

+1 for the proposal.

In addition to this alternative:

struct Foo: ?Copyable {
}

I'd like to add these two alternatives:

struct Foo: !Copyable {
}

or

struct Foo: not Copyable {
}
1 Like

I would push back against any notation that suggests "not", because it wouldn't generalize to any generic constraint contexts other than the type declaration itself. We will eventually also want to able to write something like <T: ?Copyable> to indicate that a generic parameter is not required to be copyable, but that doesn't mean that T isn't copyable, since you could still use a copyable type as the type argument.

11 Likes

I don't see how one precludes the other. Here you are talking about some "<T: maybe Copyable>" (sorry for another straw man syntax) which looks different to "struct Foo: not Copyable" both in the form and in the meaning.

SE-0390 Noncopyable Types Release Notes

In conjunction with the Swift Evolution Review of SE-0390 “Noncopyable Types,” we have a prototype implementation that has been merged into the main branch of Swift on Github and will be available in the next Swift toolchain snapshots behind the -enable-experimental-move-only flag.

There are some known limitations of the current prototype. Some of these are issues we intend to address very soon; others are outside the scope of SE-0390 and will be dealt with as part of future Swift Evolution proposals.

Short-term issues . These are all limitations of the current prototype that we expect to address soon. We hope to fix most of these before the review is complete.

  • Noncopyable types are currently introduced by marking a struct/enum with the attribute @_moveOnly instead of @_noncopyable
  • A property that holds a noncopyable type is not yet fully resilient. If you expose a type containing such a property as part of a framework’s public API, attempts to read the property may attempt to copy the noncopyable type, leading to a runtime crash.
  • A noncopyable type that is generic and has properties whose type depends on the generic type parameter are compiled incorrectly. This means that the compiler /may/ attempt to emit copies of such types and thus result in copied noncopyable type errors emitted by the compiler.
  • You cannot form an Optional containing a noncopyable type. In order to simplify certain use cases, we intend to support Optionals, even though other generic types will not be supported initially (see below). To work around this, one can create non-generic concrete enums that simulate as if one had a generic optional.
  • Struct and enum deinit methods are not yet included in the value witness. This should only be visible to users of the current implementation when capturing a var in an escaping closure and passing the var inout. In such a case, the deinit may not be called.
  • Indirect enums cannot be marked as non copyable and enums cannot have indirect cases with noncopyable payloads.
  • The compiler may crash when compiling with -O,-Osize deinits of noncopyable types that only contain trivial fields.
  • The test toolchain does not yet support autodifferentiation.
  • It is currently possible to switch on a noncopyable enum with deinit or destructure a move only struct with deinit. This will cause the deinit not to run. In future versions this will be banned.
  • The forget keyword has not yet been implemented
  • At -Onone, we shrink lifetimes aggressively towards uses in certain cases rather than shrinking aggressively and then maximizing lifetimes as much as we can like we do with other lexical lifetime values.

Deliberate restrictions . These are limitations that we do not expect to address in the current work, either because they are fundamental to the model or because their implementation requires design work that will need to go through additional Swift Evolution discussion and review.

  • Noncopyable types cannot be used as generic type arguments. For example, you cannot form an Array<N> where N is a noncopyable type. We are exploring approaches and hope to begin a Swift Evolution of this in the near future.
    • As a special case, we do expect to support Optionals containing noncopyable types very soon.
  • Noncopyable types cannot conform to protocols. This is a consequence of the limitation on using noncopyable types as generic type arguments above, and we expect to address it as part of the same effort.
    • As a special case, we allow noncopyable types to be Sendable with the usual Sendable restrictions.
  • Noncopyable types cannot be stored in existentials, including Any, AnyObject, or any Sendable.
  • There are limitations on using noncopyable types in if..let, if..case, switch statements, and for loops. We have pitches for these under discussion [1] and expect to prototype this support soon.

[1] https://forums.swift.org/t/pitch-borrow-and-inout-declaration-keywords/62366

10 Likes

+1. This is an important feature that will bring the benefits of value semantics to cases where a resource must be cleaned up after its last use.


I don't love the term "copy", though. When I think of a copy, I think of things like copying a buffer — taking the contents of one buffer and duplicating them within another buffer. I don't think of things like doing atomic reference counting operations. This loading of the term "copy" will result in confusion in cases like this (once we have the copy operator):

1> let a = Array(repeating: 0, count: 512)
2> var b = copy a
3> b[2] = 1

It'll be difficult to explain to a beginner that the array's contents are not copied in line 2, but that they are copied in line 3.

Seeing that we chose the term "consume" instead of the more popular term-of-art "move" to avoid confusion, I think we should consider using a different term for "copy" as well.

Some possible replacements are:

  • clone (I believe Rust uses this term for a similar operation)
  • duplicate
  • recreate
  • replicate

Edit: changed a let to var in the code sample above, thanks tera!

4 Likes

FWIW, these all have exactly the same implication as “copy” to me (unsurprisingly, since they’re synonyms).

2 Likes

The array is semantically copied on line 2, and the fact that the implementation puts off copying the contents to line 3 is an optimization. A beginner shouldn't need to know the implementation details at that level of granularity.

11 Likes

What does "semantically copied" mean?

The only reason a and b in that example do not affect each other is because Array has value semantics. For anything with reference semantics, the result is quite different, and most programmers would not call it a "copy".

Readers cannot know what line 2 actually does (and hence what the result of line 3 might be) without in-depth knowledge of the types involved and which semantics they implement. It defeats local reasoning.

I don't think these are beginner features; beginners should stick to copyable types and implicit copying. Today, users tuning their code for performance (which is a job that requires advanced understanding) often do need to be aware of copy-on-write, though.

4 Likes

Should it be "var b" on the second line?

Where do I read more about the "copy operator"? Does it apply to both value and reference types?
As written I don't quite understand the example above, as even with "var b = a" it would work as expected (with copy being created ((COW complications aside)). Did that example mean to show something unusual on the first line? like this?

@noncopyable let a = Array(...)

I also wonder what the equivalent example would be for reference types. For which copy would (supposedly) mean retain.

If we need a word which is both similar to "copy" but at the same time doesn't not have its connotations, and there is no English word for that – I guess we just invent our own word.. Not a big deal :slight_smile: For the purposes of discussions we may use some placeholder word, even "xyzt", and after agreeing on everything else it would be just a matter of choosing a proper name for it, which would be the easy part.

1 Like

I think adding noncopyable types to Swift makes something clear: everything in Swift, up until the addition of move-only types, had "value semantics", which is why I've argued that "value type" isn't a well founded concept in the past. The value of a "reference type" is the reference, and copying the value copies the reference, and that holds up for any definition of "value type" that has been put forth. It's the implicit copyability, the ability to create semantically identical and completely interchangeable values (aside from consuming the time and resources necessary to make the copy) that defines a value type.

7 Likes

In my interpretation, there are multiple "copy" words here, and this is a source of confusion:

  • There is "copy", as seen by the compiler implementor (let's say level C). Here copy can be used for bit-wise copy, and also for incrementing the retain count of a class instance.
  • There is "copy" as in COW, seen by the advanced Swift user (let's say level A). Here copy only refers to the (expensive) memory operation, and COW is an optimization technique that avoids this cost.
  • There is "copy" as in "b is a semantic copy of a when the var b = a statement is executed". I'm not sure many Swift beginners would firmly use the "copy" word to describe this operation - they're beginners after all. This is the "copy" of the compiler lawyer (level L), sometimes used to enlighten both beginner and advanced users, and introduce them to the subtleties of both level C and A, but mainly for grounding the compiler principles and allow it to gracefully evolve on the golden path of efficient memory-safe languages. It Swift had an abstract virtual machine, this "copy" would belongs there.

And finally, there's the "copy" of @noncopyable. This one (in my understanding) belongs to level C - but maybe level L. This surely creates confusion, because most people who have the curiosity to understand what the @noncopyable incantation means come from the level A. But it's not at all about COW.

I did not find "move-only" that bad. At least there's no confusion due to the multiple meanings of "copy".

2 Likes

One of my concerns about "move-only" is that we've never actually surfaced the term "move" anywhere else in the language—even the operator that moves things, we ended up calling "consume".

8 Likes

i'm definitely a ‘A’ user and personally i don’t find the word “noncopyable” confusing at all. maybe it would make more "sense" if we called it "nonretainable" but i think that would be even more confusing since the purpose of noncopyable is to store things inline that would otherwise need a separate allocation.

to me my biggest issue with the proposal is the inevitable proliferation of "Maybe__” optionoid types and unjustified ExpressibleByNilLiteral conformances that are going to show up in libraries and have to go through painful deprecation cycles. but the word “copy” is quite low on my list of concerns.

1 Like

@consume-only, then? :sweat_smile:

(I'm not serious: I really don't know, and I do understand the naming challenge)

Optional will shortly get support for noncopyable types, even if this is a short-life hack (I don't remember where I saw that). I wouldn't be concerned about this: the compiler team is quite aware that this is dearly needed.

1 Like

if it lands in a swift release, libraries will adopt it, and sprawl out whatever API they need to support it. that API will inevitably need to be unwound and deprecated if Optional ever becomes noncopyable-compatible. that is just the reality of the package ecosystem.

2 Likes