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

Wouldn’t this mean that we could never support clearly disjoint conformances to the same protocol without having to revisit the design of Copyable?—I don’t think we’d want to build our design here on the premise of foreclosing such a future direction unless our hand is forced.

2 Likes

I could not agree more. I very strongly encourage a spelling in plain English to convey the exclusion of the implicit conformance rather than a symbol. I realize it's more to type, but clarity is so much more vital than brevity. What that best word is I'm not sold on yet, though @benlings without Copyable suggestion certainly has merit.

8 Likes

This topic is well over my head but just wanted to put a vote in for !Copyable. All the Swift I have learned so far ! Fits the rest of the language for me.

For generics-heavy code like the standard library, how prevalent would you expect ~Copyable to be? Or, to put it another way, if we didn't have the status quo of implicit Copyable, would there be more declarations where you'd have to add : Copyable (with the consideration that some protocols would already imply the Copyable constraint) than where you'd now have to add ~Copyable? My guess is that you'd be removing Copyable more than you'd be adding it – that you'd want ~Copyable on pretty much all types that take generic arguments and pretty much all generic functions, and that Copyable conformances would mainly be declared in conditional extensions.

If we draw parallels to the Objective-C nullability transition: NS_ASSUME_NONNULL was an appropriate tool for that task because the previous default of "assume everything may be null" didn't apply to most code written in the new nullability-aware world; in that scenario, rather than requiring _Nonnull everywhere, pragmas were introduced to swap the default. My question is whether we're in a similar situation here – whether "assume everything is copyable" may not be the right default for the new copyability-aware world – and, if it's not the right default, what tools do we have other than adding ~Copyable everywhere to shift that default, or to at least lessen the annotation noise?

Digression on preferred defaults for ownership/copyability aware code

For what it's worth, if we were starting over the defaults I'd pick for an ownership/copyability-aware dialect would be:

  • Types are implicitly Copyable within their module if all of their members are Copyable (so e.g. struct SomeStruct<A, B> { let a: A; let b: B } is copyable if A and B are copyable).
  • Types are non-Copyable outside their module unless they have an explicit Copyable conformance.
  • Generic arguments are by default non-Copyable.
  • Ownership qualifiers (borrowing and consuming) are required for all non-TriviallyCopyable arguments.

I'm not sure how close we can get to that without creating dialects or majorly breaking source compatibility. I consider that a desirable enough end-goal that I'd want a dialect that did that, but maybe a linter or a compiler flag for warnings would be enough.

The standard library is not very representative of general Swift code. But for what it’s worth, the standard library has a lot of copyable types.

Speaking just for myself, I think biasing towards copyable types is absolutely the right thing for the language to do. Non-copyable types are substantially more challenging to work with and really shouldn’t be pushed on most programmers. They also encourage programmers to become hyper-focused on eliminating micro-scale overheads, most of which are not relevant to their program’s performance. We want to provide this feature, but we should not confuse that with wanting to recenter the language around it. It is a tool that should be used judiciously in places where it is truly important.

That doesn’t mean we should deliberately make it frustrating to write non-copyable types, but it does mean that I’m more prepared to accept a higher level of syntactic burden for the people opting into this.

16 Likes

I like ~Copyable when it's used as a generic constraint. To me, it seems quite clear in the context of the language that this means that some assumptions about the type are taken away. However I really don't like the use of the same syntax for noncopyable types, because there it is not only taking away assumptions but it's actually adding a new capability also, namely the capability to define a deinit for this type. For me at least, this doesn't align well with this syntax.
Also, even if it isn't part of the proposal right now we should still consider the following scenario:

struct Foo<T: ~Copyable>: ~Copyable {
    let bar: T

    deinit {
        // do something special with bar
    }
}

extension Foo: Copyable where T: Copyable {}

Does Foo now have the deinit even when it is Copyable? Is it not allowed to have a deinit in a conditionally copyable type? If yes, should we maybe have a different syntax for those types then? I'm not asking these questions because they must be thoroughly answered right now. But it could be possible that they influence the syntax for this proposal so we should at least consider them.

This is correct. deinit is reserved to types that are not copyable at all.

1 Like

I'm not able to think about a concrete example right now, but I could imagine that it could be totally possible that someone would want to write a conditionally copyable type that has a deinit if its generic parameter is noncopyable. Do we want to exclude this future possibility?

1 Like
Are we sure that a protocol is the right way to model copyability?

I've hidden this text to minimize the noise I may add to the thread, specifically because it was stated clearly that the proposal has been accepted in principle and the only feedback being sought is on the exact punctuation mark to be used, but I had a somewhat negative reaction just now when reading the proposal because of how many aspects of the current design seem special-case-y/convoluted, so I wanted to at least say that and explain why (in a non-obtrusive way):

  • The proposal starts out by introducing the ~Copyable syntax for declaring a struct or enum as not-yet-copyable. The syntax makes Copyable seem mostly like a protocol (except for the somewhat unsettling new ~ sigil), but so far it doesn't need to be. We could imagine many variations on the current struct/enum declaration syntax that gives us the option to declare them as noncopyable.

  • The proposal then goes on to describe the rules about composition involving noncopyable types. Still nothing protocol-y going on in this section. It's more reminiscent of the constellation of rules that were being discussed when actors were still slated to be melded with class-inheritance (thanks to Chris Lattner for helping us avoid that).

  • Then the proposal describes the various (sweeping, even) ways in which Copyable cannot be used like a normal protocol. At least now the modeling as a protocol is acknowledged, but rather than being backed up as being the best approach it seems to me that that argument is weakened.

  • Then comes the section titled "The Sendable Exception", in which the proposal expands upon more special-case-rules that the design obliges.

  • Then a section about some workarounds for the limitations with generics.

  • Then a lot more (involving consume, borrow, etc.), which I don't have time to fully process, but which isn't necessary for me to process yet because I'm just making the case that the proposal brought up a major doubt for me (why Copyable is modeled as a protocol-ish-thing), which was only compounded as I continued to read, so if my concerns are clearly addressed further down in the proposal and I missed it by only reading half of the proposal then my only suggestion is to move that part higher up in the document so that the proposal doesn't have to be processed all at once in order to seem solid to the reader (and in which case I would also be glad I minimized the visual impact of this critique on this thread).

3 Likes

Perhaps I'm missing something - is it not possible to add conditional conformance to Copyable outside of the module? Because if it were possible it seems like this would be impossible to enforce

No, it is an inherent constraint on the type, like Sendable, and conformances are limited to the file that defines the type. That should definitely be in the proposal if it is not.

5 Likes

It's not. Copyable will ultimately be a "layout constraint", which is an intrinsic property of the type. AnyObject is another example of this concept. The proposal is careful not to refer to Copyable as a protocol, but maybe it's worth spelling this out. It'll still behave mostly like a protocol, in that you can use it as a generic constraint, but can't add conformance to it if the type fundamentally doesn't satisfy its requirements, just like you can't declare an arbitrary type to conform to AnyObject.

Sendable is still barely a protocol, since you can retroactively add conformance to it (though the reasons to do so will hopefully fade over time).

8 Likes

If we can think of a use case in the future, then disallowing it now shouldn't be a permanent limitation, since adding custom deinits to more kinds of type would fit naturally into Swift's implementation model.

So if I understand correctly, the reason we are using an operator prefix for the protocol instead of calling it "NotCopyable" is because "Not" isn't a very good description and we couldn't think of a better one?

Furthermore, the reason we are using ~ instead of ! Is because ! is too easily read as "not" whereas ~ can be ambiguously interpreted as either "not" (as in bitwise arithmetic) or "sort of" (as in ~=) or "maybe" and we'd actually prefer to be ambiguous between all those rather than pinning down the meaning?

Surely that's the exact opposite of how it's supposed to work - the justification for using an operator instead of a keyword is normally that it's a well-known term of art with an unambiguous meaning.

How are people going to talk about or explain or understand this feature if it's not even possible to name it? Will you go around correcting people who pronounce it as the "NotCopyable" protocol? Will the official guidelines say to call it "TildeCopyable" instead so we don't accidentally imply some particular behavior? This all seems absurd.

Swift has in the past been willing to choose very explicit protocol names, even at the sacrifice of memorability (ExpressibleWithStringLiteral springs to mind), so why not just call this protocol "MovableWithoutImplicitCopy" or something similar?

8 Likes

@nicklockwood I came from that school of thought initially, it took a bit to grok it but basically:

What is under discussion is an operator on the Copyable constraint, with the operator meaning that the affixed constraint is not being implicitly provided. That's why it makes sense as an operator rather than some explicit name IMO. It also scales better, so in the future when we can suppress other implicitly provided constraints/conformances, you just reach for the operator that means that concept.

With explicit names, you'd need a whole bunch of arbitrary spellings to cover every case of (eg) 'MovableWithoutImplicitCopy', 'ValueWithoutImplicitSendable', 'EquatableWithoutImplicitEquatable' and whatever else.

I read ~Copyable as "maybe Copyable", or -Copyable as 'subtracting implicit Copyable'.

(Hopefully this helps explain why a name like 'NotCopyable' would be inaccurate (the original pitch chose @noncopyable, also inaccurate for the same reason) ... because an extension could later add Copyable to the same type that has been declared as 'NotCopyable'. The operation is really a suppression of a default behavior, rather than denial.)

2 Likes

The scalability aspect makes sense as a justification for not changing the protocol name, but not for using an operator. A new keyword or attribute would serve the function better.

If we are saying the operator should be read as "maybe" then why not use a "maybe" keyword? And if "maybe" isn't actually the right word then we're back to my original point that we're using an operator to paper over the ambiguity rather than eliminate it.

Also, since other use cases for this syntax are speculative at the moment, until they've actually been pitched it seems like the rule of three / YAGNI applies. Why not just make it a separate protocol name for now and deprecate that name later in favor of the operator/keyword/attribute that replaces it?

6 Likes

On keyword vs operator, I don't have a solid argument for or against. I personally lean towards the operator terseness being more appropriate here, but I'm not beholden to it.

I don't think the other use cases are that speculative. They seem pretty likely to happen, eventually.

1 Like

In my mind, part of the issue is that the behavior we're trying to describe isn't ambiguous so much as it's difficult to describe concisely. The best suggestions are along the lines of suppress but I really haven't yet seen a great one-word phrase that captures what's going on here, and I don't think I would want a longer phrase like removeAssumedConstraint(Copyable).

I do concede that a keyword is generally easier to search for than an operator, but I expect "tilde protocol" or "minus protocol" would yield decent results once this feature is out in the wild?

Whatever we do here, I really don't think we should make this look just like any other protocol. I think it's important for the mental model that we somehow try to communicate visually that copyability is the 'positive' constraint rather than treating noncopyability as something that is somehow added to a type.

4 Likes

The one true and obviously correct spelling is to write struct Foo: implicn’t Copyable

20 Likes

Then "-Copyable" is better than "~Copyable". "~" has obvious "not" connotations (from bitwise operations in swift and from math in general" – in that it can change true to false and it can change false to true. OTOH "-" is strictly removing (unless we are talking about wrapping arithmetic, and we aren't). As with math "+" is optional and we don't put "+Equatable" (although if to follow the precedent established by math we could).

Edit: the second connotation of ~ is "approximately" – also NOT the right meaning for what we are trying to convey here, which is: "sans copyable" / "suppress default copyability" / "without adding default copyability".

So, neither meaning of ~ seems to be applicable here: neither not, nor invert, nor approximate.

2 Likes