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

Hello, Swift community!

The second review of SE-0390: Noncopyable structs and enums begins now and runs through April 4, 2023.

This proposal has already been accepted in principle. We would like to keep this review narrowly focused on the two significant changes from the first proposal:

  • The new syntax for suppressing the implied copyable constraint. The authors have used Type: ~Copyable for this purpose. During the initial review, a few variants on this were proposed; the workgroup is willing to consider alternatives of the form xCopyable, where x is a printed ASCII character available on US keyboards that doesn't obviously mean something else. Under my survey, the set of vaguely-plausibly-viable characters is ~, -, /, ?, ^ and !.

    The authors have used ~Copyable; absent a clear case that one of the other options would be better, that is what we will use, but we are open to argument.

  • The operator formerly known as forget is now discard self has been tightly constrained:

    discard self can only be applied to self , in a consuming method defined in the same file as the type's original definition
    ...
    For the extent of this proposal, we also propose that discard self can only be applied in types whose components include no reference-counted, generic, or existential fields, nor do they include any types that transitively include any fields of those types or that have deinit s defined of their own. (Such a type might be called "POD" or "trivial" following C++ terminology). We explore lifting this restriction as a future direction.

    and its semantics have been more tightly specified. The LWG would especially appreciate focused attention on these details from reviewers.


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 by email or DM. When contacting the review manager directly, please keep the proposal link at the top of the message and put "SE-0364" in the subject line.

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,

Steve Canon
Review Manager

14 Likes

Is the new requirement suppression syntax only for ~Copyable or could it be used for any protocol? Or perhaps any implicit conformance? Can a class be defined to be ~AnyObject? (I have no idea why anyone would want to do that. I'm just asking if this new syntax is universal or explicitly just for Copyable.)

~Copyable is not a generalized protocol-suppression mechanism.

See "suppressing implicitly derived conformances with ~Constraint" under alternatives considered for a more generalized form, but even there we would not extend this to suppressing arbitrary conformances.

1 Like

I wasn't involved in the initial review, but was a separate keyword considered? Much how any T makes Existentials clear in plain English without having to guess at esoteric symbols, a clear English spelling of not Copyable seems like a very straightforward and clear spelling of the intent here.

Someone reading code with struct FileDescriptor: ~Copyable {... in it without having read this will have to stop and ponder what that tilde might mean, and while they may correctly reach the conclusion that it is exclusionary, anyone reading struct FileDescriptor: not Copyable {... in code would require no prior knowledge or guesswork.

5 Likes

A key point, I think (which tripped me up for some time until it was put to me more clearly), is that we aren't exactly going for the meaning of not Copyable: rather, it's more "maybe Copyable, but not by the default way."

That is to say, in the future, we envision a scenario where a type could be conditionally Copyable with a syntax such as:

struct S<T>: ~Copyable { ... }
extension S: Copyable where T: Copyable { ... }

For this reason, ?Copyable was floated as an option during the last pitch, in contrast to the spelling !Copyable that has been used as a strawman for "categorically not Copyable" and--I'd surmise for that reason--was not the syntax chosen for this revision. Something like not would have the same pitfall in my view.

The symbol ~ is used elsewhere in Swift as a "bitwise not," and for that reason I'm a little on the fence about it here. It's been said that an alternative reading of ~Copyable based on other usages of the symbol could be "Copyable-ish", which does convey a more on-the-nose meaning. It would seem to me that if you're taking away the meaning of "not Copyable" from the notation proposed then there are some grounds to suspect that others might too.

For myself, I'm rather partial to -Copyable as an alternative; for me, this strikes the right tone that we are "subtracting" the (implicit) Copyable conformance, which does not foreclose the future possibility that an extension could add it back nor force that notation to be seemingly contradictory.

20 Likes

I really like this syntax and the reasoning you've given behind it. Though subjective, I think it feels cleaner/less like "punctuation soup" than ?Copyable.

At best, the weakest counterpoint I can think of is whether - is too subtle when reading it, and/or that -Foo could be misread as _Foo. But, this is only being proposed for a single constraint right now so users would be trained to read -Copyable as a single concept and not a concatenation of two concepts, even when the positive Copyable is used later to re-add the constraint. And even if this subtraction syntax is expanded in the future to cover other implicit conformances, there are so few of those that I think eventually readers would similarly read something like -Equatable as natural as well.

1 Like

BTW, the spelling: maybe Copyable is clearer than ~Copyable or any ascii symbol prefix.

11 Likes

"Maybe copyable" actually has somewhat the wrong sense too, however. However we spell it, the pronunciation is really "without the copyable constraint" or "with the copyable constraint removed."

struct Type: ~Copyable { ... }
"Define a struct Type without the [implicit] Copyable constraint."

4 Likes

I'm trying to fully understand the desired behaviors of removed implicit constants and potential negative constants. In my head the range behaviors could be:

behavior conformance constraint
implicit A. struct Foo E. struct X<Bar>
positive B. extension Foo: Copyable F. struct X<Bar: Copyable>
optional C. extension Foo: ~Copyable G. struct X<Bar: ~Copyable>
negative D. extension Foo: !Copyable H. struct X<Bar: !Copyable>

A. Foo is implicitly Copyable
B. Foo is explicitly Copyable
C. Foo is not implicitly Copyable

  • Future direction: but could be made retroactively Copyable. (I don't think this makes sense)

D. Foo is explicitly not Copyable and cannot be made Copyable

  • I like this because it could work nicely with !Sendable, !Reflectable. Note: and maybe even standard protocols like !Equatable (though I'm not sure why one might want this).

E. Bar implicitly must be Copyable
F. Bar explicitly must be Copyable
G. Bar may or may not be Copyable, implicit constraint removed
H. Bar explicitly must not be Copyable.

  • I'm not certain this is a sound constraint, and certainly not for a general negative protocol constraint.

I see the use of ~Copyable as pitched covering either options {C or D} and G. However, if "Suppressing implicitly derived conformances with ~Constraint" is a serious future direction, then I don't see how ~Copyable can be made to mean option C, because it seems unsound to opt out of implicit Copyable then opt back in (either in the same or different module), e.g.:

struct Foo { }
extension Foo: ~Copyable { } // not implicitly Copyable
extension Foo: Copyable { } // explicitly Copyable

Additionally, it (perhaps naively) seems like it would be valuable to be able to spell option D to clearly state the programmers intention that Foo is explicitly not Copyable and never should be.

I'm curious that the author's and LWG's thoughts on the options we'd want to expose in the language are.

2 Likes

I like to see that the proposal is now using the protocol name to define non-Copyable types. The discard operator looks good and I agree it fits better with Swift terminology. I only have two questions/suggestions.

For the name I’d like to see either dropimplicit Copyable which clearly spells out the operation. I’m also fine with a more succinct version: ?Copyable, which I see as saying that the implicit constraint is now dropped, and the conformance to Copyable is optional.

My other suggestion was about the protocol hierarchy. I wonder if it would make sense to rename the current version to ImplicitCopyable and introduce a new “explicit” Copyable protocol where ImplicitCopyable : Copyable. In this assortment, Copyable would have a simple copy() requirement that can be synthesized by the compiler, but not automatically inserted to implicitly copy types. I imagine this being useful for low-level types, like an array without COW, that still provide a copy option, but its high cost is conveyed through the explicit copy() method. I also think that a generic constraint allows algorithms to operate with these types while making it clear when expensive operations occur.

This would leave us with ImplicitCopyable for regular types which should have relatively inexpensive copy operations. Moving up the hierarchy, low-level environments could use Copyable to safeguard performant algorithms against expensive implicit copies. And lastly, types that truly shouldn’t be copied, like a file descriptor, would conform to neither protocol.

I know this would be a big change for the proposal, but I hope we can leave room for such a direction (to avoid ABI breaks) and consider it as a future direction.

Other than that, great work!

Then @xwu's suggested -Copyable makes even more sense IMHO, it's a subtraction of something, not a negation / not.

3 Likes

Must the syntax be specifically tied to the existing protocol conformance spelling (A: B)? It seems like trying to jam something that removes “conformances” into the same box that adds them is always going to be awkward.

Just spit-balling one possible option, how about a new “operator” like -:? This could be limited to Copyable for now, and the spelling could then look like struct Foo -: Copyable. Definitely introduces some awkwardness when trying to both add and remove constraints to a single type, but maybe that could be resolved by either always forcing the removal of implicit conformances to be in a dedicated extension, or with some forced ordering of adding and removing (struct Foo: Codable, -: Copyable).

I’m really not trying to pitch anything concrete in that area right now (I don’t love any of my suggestions above), mostly just exploring if there’s a way to decouple adding and removing constraints from having to use the same syntax, which to me is the root of why there’s no clear winner for the options currently being discussed in this thread.

4 Likes

Maybe this?

struct Foo: discard Copyable { ... }

and later on retrofit discard into this as well:

func foo() -> discard Int { ... }
// instead of @discardableResult
1 Like

"Without the copyable constraint" sounds a little bit technical to my ears, like an implementation detail of the compiler. Isn't the high-level concept that we want to express "regardless of the copyable conformance" instead?

I asked ChatGPT for an adverb or adjective that expresses our concept. I have removed the start of the chat in the transcript below:

[...]

GR: Here "copyable" is a technical term of the Swift programming language. We're not quite at the language level of laypersons. So "freely" and "unrestricted" are not precise enough.

ChapGPT: [...] you could use "copyable" with a negating prefix, such as "non-copyable" or "uncopyable," to indicate that the constraint has been removed.

GR: What if I want to express that BOTH copyable and uncopyable are accepted?

ChapGPT: [...], you could use a phrase such as "copy-optional" or "copy-agnostic" to indicate that the copyable constraint is not a significant factor and that the object can be used with or without copying. [...] These terms suggest that the ability to copy the object is not the main concern, and that the object can be used in a flexible manner.

GR: That's interesting. And now what if we look for a single character that expresses this idea of agnosticism? We want to write xCopyable, where x is a printed ASCII character available on US keyboards that doesn't obviously mean something else. Under my survey, the set of vaguely-plausibly-viable characters is ~, -, /, ?, ^ and !. A suggestion?

ChatGPT: Of the characters you listed, the tilde (~) might be the most appropriate choice for expressing the idea of copy-agnosticism. The tilde is often used in programming languages to indicate negation or logical complement, so using it as a prefix to "copyable" suggests that copying is not a necessary or primary concern. For example, you could use "~Copyable" to indicate that the object is copy-optional or copy-agnostic. The other characters you listed might have other meanings or connotations that could cause confusion or ambiguity, so the tilde may be the most clear and appropriate choice in this context.

GR: I follow. But the question mark also conveys optionality. Why is the tilde better than the question mark?

ChatGPT: You're right that the question mark can also convey optionality, such as in optional parameters or optional values. However, in the context of "xCopyable", using the question mark might imply that the copyable property itself is unknown or uncertain, rather than being intentionally left open to interpretation. Additionally, the question mark is often used in regular expressions or as a wildcard character, which could cause confusion or misinterpretation in a programming context.

On the other hand, the tilde is more commonly used to represent negation or complementation, which aligns with the idea of a "non-copyable" or "copy-agnostic" property. Therefore, using the tilde as a prefix in "xCopyable" could more clearly convey the intended meaning of the property being flexible or optional.

Folks, this tool is fun :slightly_smiling_face:

Copy-agnostic, ~Copyable, anyone?

EDIT. I asked again, this time asking ChatGPT to consider this whole thread, all arguments given for or against alternatives to ~, and the full proposal. This gives:

ChatGPT: As an AI language model, I cannot make any definitive recommendations or decisions regarding language design choices, as those ultimately depend on the Swift community and its priorities. However, based on the arguments and considerations outlined in the proposal and the forum thread, a possible approach to choosing a marker for non-copyable types could involve considering the following factors:

  1. Visual distinctiveness: The chosen marker should be visually distinct from other operators and characters in Swift to help users identify non-copyable types easily.
  2. Familiarity: The chosen marker should be familiar to users of Swift and/or other programming languages to minimize confusion and promote adoption.
  3. Consistency: The chosen marker should be consistent with existing Swift language design principles and conventions to promote coherence and maintainability.
  4. Future-proofing: The chosen marker should not hinder future evolution of the language, such as by precluding the use of the marker for other purposes or by limiting the ability to add new features.

Based on these factors, a possible option for a marker for non-copyable types in Swift could be the tilde character ~, as it has visual distinctiveness, is used in other programming languages for bitwise negation, and is a rarely used operator in Swift. However, this choice would need to be evaluated in the context of the Swift community's priorities and concerns, and further discussions and experimentation may be necessary to arrive at a consensus.

1 Like

I still have concerns about how well something like ~Copyable could scale, and think we should be seriously considering a syntax that can apply to more than one type at a time. I think something like a @suppressImplicit(Copyable) attribute could serve us better in that context.

For noncopyable-aware code, the implicit Copyable constraint on generic functions (and likely type arguments) is likely to be the wrong default moving forward. For types, you generally want them to be copyable unless there's a good reason for them not to be; for generic functions, you generally want them to allow non-Copyable arguments unless you explicitly need to copy them. Annotating every generic argument with ~Copyable would be unnecessary visual noise, and not doing so would lead to the wrong default. I'd worry that we'd end up with a similar situation to people (including me) applying @inlinable to every function in the absence of -cross-module-optimization.

On the other hand, if we had something like @suppressImplicit(Copyable), then the following:

struct NonCopyableStruct: ~Copyable {}

func allowsNonCopyable<T: ~Copyable>(argument: T) {}
func requiresCopyable<T>(argument: T) {}
func requiresCopyableA<A, B: ~Copyable>(a: A, b: B) {}

would instead become:

@suppressImplicit(Copyable) 
struct NonCopyableStruct {}

@suppressImplicit(Copyable) 
func allowsNonCopyable<T>(argument: T) {}

func requiresCopyable<T>(argument: T) {}

@suppressImplicit(Copyable)
func requiresCopyableA<A: Copyable, B>(a: A, b: B) {}

which is a little verbose but unambiguous. The flexibility of this approach, however, means that could optionally be equivalent to something like:

@begin(suppressImplicit(Copyable)) 
// meaning "apply `@suppressImplicit(Copyable)` to everything in this scope"

struct NonCopyableStruct {}

func allowsNonCopyable<T>(argument: T) {}
func requiresCopyable<T: Copyable>(argument: T) {}
func requiresCopyableA<A: Copyable, B>(a: A, b: B) {}

@end(suppressImplicit(Copyable))

where the suppressImplicit(Copyable) directives could be omitted with a -suppress-implicit=Copyable compiler flag, which is where I think most ownership-aware modules would end up.

It's also worth noting that most generic arguments would still be copyable if they're at all constrained – for example, a Scalar: BinaryFloatingPoint argument would be copyable because BinaryFloatingPoint would presumably conform to Copyable.

The main caveat with this approach is that you might not know you're in a noncopyable scope from glancing at a section of code. However, I think there'd be other indicators to mitigate that – ownership markers on parameters, for example, and the fact that you'd very quickly get a compiler error when you try to copy a not-implicitly-copyable type. On the other hand, a generic function that doesn't accept noncopyable types gives no indicators of that when you write it; you'd instead find out later when you try to use it with a noncopyable type.

6 Likes

An attribute such as @suppressImplicit(Copyable) suppresses the Copyable requirement for all arguments of a function, right?

But we still need to be able to express:

// Only one type is ~Copyable, the other is not
func foo<T: ~Copyable, U>(...)

Oh, I see, you'd add an explicit Copyable constraint to U. OK, it works.

1 Like

I like this @suppressImplicit idea because it side steps the whole issue of explaining a "negative conformance". But how would it work for classes?

@suppressImplicit(Copyable)
class Something<T> {}

Here the goal is to avoid T being copyable, but our class Something gets caught in the middle. I suppose the compiler will emit an error (because it doesn't support non-copyable classes) and the fix would be to add a Copyable conformance explicitly:

@suppressImplicit(Copyable)
class Something<T>: Copyable {}

The special thing with classes is they are always copyable even when containing non-copyable elements (because the copy is simply a copy of the reference). Should class declarations get an exception and this Copyable conformance would be implicit despite the @supressImplicit? Or do classes need an explicit Copyable too?

The same question applies to actors.

And nested types?

@suppressImplicit(Copyable)
struct FileDescriptor {
    var handle: Int

    enum State { // is this copyable?
       case open
       case closed
    }

    func write<O: OutputStream>(to output: O) {
       // is O copyable?
    }
}

In my opinion it shouldn't apply to nested types unless they have their own @suppressImplicit(Copyable). And it shouldn't apply to generic parameters of methods either. But different people might have different ideas.

In other words, @suppressImplicit has a very clear meaning and looks good to me, but how wide it should suppress Copyable is a bit unclear. ~Copyable has the advantage of having a more local effect, but has the downside of reading sort of like a negative conformance which makes it less easy to reason about.

In the past, the Core Team and Language workgroup have taken strong stands against introducing dialects where the language behavior is simply different forever based on flags. I do not foresee that being different here. Do not design around the idea that there will ever be a flag that makes Copyable requirements be suppressed by default.

11 Likes

Could you clarify what you mean by “scale” here? ~ costs one character per negated protocol, while @suppressImplicit adds a one-time cost of 17 characters (plus probably a newline), so while technically I suppose you might be concerned about the added source code size impact of needing to negate more than 17 implicit conformances, I suspect there’s something else you mean.

Although this proposal limits the ~Constraint syntax (or whatever operator or keyword we choose for ~) to ~Copyable, that's only because that's as much as we felt we could get implemented within the scope of this feature. There is at least one other immediate use case that it could be applied to: the implicit Sendable conformance for internal types. Currently there is no way to suppress the inference of Sendable (other than indirectly, by declaring that it is explicitly Sendable in an extension that's unavailable), but we'd like this syntax to be applicable there as well. Sendable also already allows for conditional conformance, so it would be useful to be able to declare a type such as:

// Although represented as an `Int32`, the value represents a resource
// that might only be usable on a certain thread, so should not generally be
// Sendable
struct ResourceHandle<R: Resource>: ~Sendable {
  private var handle: Int32
}

// ...unless the resource is explicitly ThreadSafe
extension ResourceHandle: Sendable
  where R: ThreadSafeResource {}

That might be another good example to consider when designing the syntax here.

5 Likes