SE-0427: Noncopyable Generics

I see that this is the simplest model that supports source-compatible modification of existing protocols and generic parameters, as well as progressive disclosure, and I compliment Kavon, Tim, and Slava for nailing it down. At the same time, though, I think it’s going to get old real fast. Resource & ~Copyable is a really strong example of this: if you have a protocol that in practice is going to be mostly adopted by non-Copyable types, it’s all too easy for someone to accidentally add API that’s restricted to Copyable Resources only, and the spelling Resource & ~Copyable feels redundant from a “names mean things” perspective. And extension List: Copyable {} is the kind of magic incantation that will be in FAQs forever; because our extension syntax doesn’t require uttering List’s generic parameters, it’s not at all obvious what the conditions are.

The @retroactive idea is good for source compatibility, but not for progressive disclosure. Maybe an opposite polarity attribute, as in “it doesn’t make sense to think about this type without considering restrictions on Copyability”? That sounds like something that could be added in a follow-up proposal, except that adding such an attribute to an existing protocol or generic parameter would be source-breaking. That doesn’t quite cover extension List: Copyable {}, though.


Concretely, can I put ~Copyable in a typealias? If I can, I’d be very tempted to write typealias Resource = any ResourceProtocol & ~Copyable, which should already work in all positions protocols do. Doesn’t help with generic parameters though.

6 Likes

We've spent a fair amount of time discussing these "ergonomics" issues internally, and never really came up with a completely satisfactory solution for eliminating this ~Copyable spam, which I agree might be slightly repetitive in practice. The proposal as written spells out the minimum model, in a sense, because it's what has to work for the feature to make sense at all, but there are certainly additional tweaks we could make. They are pretty straightforward to implement, and mostly independent of each other, I think. I'm not sure I love any of them, but I could certainly live with them:

  1. We could say that an extension of an unconditionally noncopyable struct or enum does not introduce any default conformance requirements. Here there are no source compatibility concerns:
struct Holder<T: ~Copyable>: ~Copyable { var t: T }
extension Holder /* no requirements */ {}  // probably what you want?

I should also mention that @xwu's proposed rule is solving a similar problem.
2. For protocols, I admit that one might intuitively think that something like defines a noncopyable T:

protocol Manager: ~Copyable { associatedtype Resource: ~Copyable }
func f<T: Manager >(_: T) /* today: where T: Copyable */ {}

This isn't source-compatible if a protocol retroactively adopts Copyable, but we could invent some way to distinguish protocols that were "born copyable", eg what a few folks are calling @retroactive ~Copyable, or as I'm going to spell it here, ?Copyable because I'm lazy:

protocol Equatable: ?Copyable {...} // strawman syntax!
protocol Manager: ~Copyable {...}

Then we could say if a generic parameter is subject to a conformance requirement to a ~Copyable protocol, we suppress the default conformance, whereas ?Copyable protocols would behave like they do today. This would be purely "syntactic" still, so eg

func f<T, U>(...) where T == U.A, U.A: Manager, U: ... /* T: Copyable still inferred here unless suppressed */
func f<T, U>(...) where T: Manager, U: ... /* T would be ~Copyable */

Similarly, protocol extensions could easily look at ?Copyable vs ~Copyable when desugaring the where clause:

extension Equatable /* where Self: Copyable */ {}
extension Manager /* no requirements */ {}

We could bring back the synthesis of the conditional conformance, but make it explicit. This was the old behavior implemented until a week ago or so:

struct G<T: ~Copyable> {} // synthesizes conditional conformance
struct G<T: ~Copyable>: Copyable {} // unconditionally copyable
struct G<T: ~Copyable>: ~Copyable {} // unconditionally noncopyable

(Now of course the first two are both unconditional Copyable.) We could of course do something like this instead:

struct G<T: ~Copyable>: ?Copyable {} // new way to request conditionally Copyable
extension G /* where T: Copyable */ {}

struct G2<T /* : Copyable */>: ?Copyable {} // probably a warning/error?

We could also say that in a conditional conformance to Copyable specifically, the where clause desugaring rules are not applied, so you'd always write out the "positive" requirements:

struct G<T: ~Copyable, U: ~Copyable>: ~Copyable {}
extension G: Copyable where T: Copyable, U: Copyable {}
extension G /* where T: Copyable, U: Copyable */ {}

This might be confusing if your conformance is only conditional on a subset of the generic parameters:

struct G<T: ~Copyable, U: ~Copyable>: ~Copyable {}
extension G: Copyable where T: Copyable {}
extension G /* where T: Copyable, U: Copyable */ {} // what?

Yes:

protocol P: ~Copyable {}
typealias PP = P & ~Copyable
func f<T: PP>(_: T) {}
5 Likes

I find this solution (and its syntax) very elegant. Much more than @retroactive. In short:

  • ~Copyable assumes type is not copyable unless you add where T: Copyable
  • ?Copyable assumes type is copyable unless you add where T: ~Copyable

I'll note that the later only makes sense for types with conditional conformance (in addition to protocols). It'd probably be best to make ?Copyable an error for types declarations that don't have a conditional conformance to Copyable.


And with this in mind, perhaps ?Copyable could be used to declare conditional conformances too. For instance, we could consider all the ?Copyable in a generic type declaration to be "linked together":

struct Array<Element: ?Copyable>: ?Copyable {}
// or:
struct Array<Element>: ?Copyable
       where Element: ?Copyable {}

Since the two ?Copyable in the declaration are linked together, Array becomes copyable when its element is. This eliminates the need for a separate conditional conformance declaration (as long as we want our type to be "assumed copyable" by default).

If you don't want them to be linked, use ~Copyable.

The plan is to make some mechanism available for everyone in a future proposal. We're currently experimenting with an attribute that ignores all inverse requirements when mangling a symbol, but it's fraught with danger...

The danger is that, if you say that a function f can now accept a generic noncopyable value and we mangle that function symbol just as it were in prior libraries, it's very hard to know whether those older libraries's versions of f truly will not copy that generic value.

Unlike a regular protocol, where you could go back through source control to see if any older versions of f called any of that protocol's methods, you can't actually verify there were no copies by looking at the source code. For a Copyable type, the compiler was free to make copies as-needed. So the version of the compiler and compiler flags (mainly optimization) used when you compiled and deployed your shared library will determine whether there are any copies of the generic value in that old library's version of f.

Under consideration is some sort of availability-based mechanism like @backDeployed so you can state "since version X, generic parameters A and B, became noncopyable" and your deployment target effectively determines how we handle callers of the function. I was thinking of having this feature be part of a separate proposal, as this one is more focused on syntax and semantics.

3 Likes

I know you said “because I’m lazy”, but before it gets too much traction I’ll say that I’m against any solution that privileges both syntaxes equally. It’s not just about learning two symbols with similar meaning; now you have to think about which one you want, instead of just going with “the default spelling”.

(Fun Swift history: we worried about this exact thing with let and var. We knew we wanted to make let the common case, at least for local bindings, but if we made it any longer than the keyword for var then some people would prefer var just out of minimizing typing. If we had been even more ambitious we could have gone Rust’s route of penalizing var, but that wouldn’t have been the best thing for struct properties, where var is usually better in practice (not to mention literally everyone was coming from ObjC, where var behavior is the default.). JavaScript has this “problem” today because they added a let that was mutable before they added an immutable const, and it’s not a huge difference in practice but it is just slightly less nice.)

3 Likes

This came to mind too when writing my reply to @Slava_Pestov. And then I made a suggestion about another way of writing conditional conformances based on the ?Copyable syntax (see reply above). And then I concluded with "This eliminates the need for a separate conditional conformance declaration (as long as we want our type to be "assumed copyable" by default)."

Well, it turns out if we used this syntax for conditional conformance to Copyable and made the one we currently have illegal, we could have one true way: types with a conditional conformance to Copyable would be ?Copyable and types with no conditional conformance would be ~Copyable. No exception.

// note: not something in the reviewed proposal, just my idea

struct Array<Element: ?Copyable>: ?Copyable {}
extension Array {} // Self is copyable here
extension Array where Self: Copyable {} // same as above
extension Array where Self: ~Copyable {}
// conditional conformance illegal (and redundant) in extension:
extension Array: Copyable where Element: Copyable {}

struct MyType<T: ~Copyable>: ~Copyable {}
extension MyType {} // Self is noncopyable here
extension MyType where Self: Copyable {} // illegal, MyType is never copyable
extension MyType where Self: ~Copyable {} // redundant
// conditional conformance illegal in extension:
// (must be put in type declaration)
extension MyType: Copyable where Element: Copyable {}

// illegal: MyType must be conditionally copyable to use ?Copyable
// use `?Copyable` in the declaration of T to make it conditionally copyable
struct MyType2<T: ~Copyable>: ?Copyable {}

No exception needed, not even for legacy types. They all work the same: a conditionally non-copyable type would be assumed to be copyable in extensions unless you explicitly suppress with where Self: ~Copyable, while an unconditionally non-copyable type is always non-copyable in extensions.

What then becomes a breaking change is adding a conditional conformance to Copyable to a type that is non-conditionally non-copyable. The reverse (making a copyable type conditionally non-copyable) remains possible.

For protocols I suppose you'd have the choice of making them ~Copyable or ?Copyable. Perhaps for coherence we could forbid ~Copyable protocols to be conformed to by copyable types (thus favoring ?Copyable in protocol declarations). I fear that's going too far though.

But at this stage this is more brainstorming than a review. I should probably shut up now.

1 Like

The proposal describes the "default where clause" idea which was originally due to @kavon. While we never figured out the semantics in full generality, I realized that what I'm suggesting in my first reply to you can also be seen as a restricted form of this. So,

struct G1<T: ~Copyable> {}
extension G1 /* no requirements */ {}

struct G2<T: ~Copyable> {
  default where T: Copyable
}
extension G2 /* where T: Copyable */ {}

protocol Manager: ~Copyable {}
extension Manager /* no requirements */ {}
func f<T: Manager> /* no other requirements */ {}

protocol Equatable: ~Copyable {
  default where Self: Copyable
}
extension Equatable /* where Self: Equatable */ {}
func f<T: Equatable> /* where T: Copyable */ {}

Basically, we would restrict a protocol's default where clause to only specify copyability on Self.

This would be equivalent to my made-up ?Copyable syntax in the two places where you could utter it.

This would not be possible (yet? help me figure out the math here so we can define the rewrite system!):

protocol IteratorProtocol: ~Copyable {
  associatedtype Element: ~Copyable

  default where Self: Copyable, Self.Element: Copyable
}
1 Like

I have re-read this thread quite a bit and can distill my confusion.

SE-390 gives this definition for "noncopyable" in its introduction:

Values of noncopyable type always have unique ownership, and can never be copied (at least, not using Swift's implicit copy mechanism)."

Note the "always" and "never". A move-only, copy-never type.

However this proposal uses the term "noncopyable" different for generics: that automatic conformance to Copyable is suppressed. It still allows for copyability as a refinement. In addition, any ~Copyable is a strict superset of all Copyable types.

Further, consider the proposal's base example:

struct Pair<T: ~Copyable, U: ~Copyable>: ~Copyable { .. }

Would documentation about the Pair type say that it "always has unique ownership and can never be copied", as described in SE-390? No, because:

// ...
extension Pair: Copyable where T: Copyable, U: Copyable {}

It seems to me that these two nuances are, at best, confusing and, at worst, incongruent.

Can we use a different term than "Noncopyable Generics" for the title of SE-0427?
Can we avoid using same syntax to express both as well?

It will be a teaching nightmare to explain struct MyMoveOnlyType: ~Copyable, but then later try to explain how struct Pair<T: ~Copyable>: ~Copyable is not declaring a move-only type.

4 Likes

There’s no inconsistency here because a generic type declaration is not declaring a single type, but a family of types, parameterized by their generic arguments. Each member of this family can be determined to be Copyable or not, and we can still talk about values of non-Copyable types as having unique ownership.

2 Likes

The identity functions in the sections “Default conformance to Copyable” and “Suppression of Copyable” are incorrect. They are both missing the return type of the function (-> T).


I haven’t read the proposal thoroughly, but I’m glad this is being addressed. The lack of generics support for noncopyable types is probably their biggest usability issue right now.

1 Like

This gave me an idea: what if the implicit copyability of a generic parameter or associated type is bound to the name of a type or protocol? Maybe that could be a simpler mental model. It would also give us a way to avoid excessive ~Copyables for a specific type within a specific scope:

typealias Optional<Wrapped: ~Copyable> = Swift.Optional<Wrapped>

Another idea is that maybe we can allow users to customize the implicit default protocol bounds in a specific scope by rebinding Any in that scope:

typealias Any = ~Copyable

It might help to talk about the same situation, just with a different protocol, and leaving aside the ~ part at first. I don't think this would be a nightmare to teach:

  • Say I create a protocol Parseable, which means that the type provides an init?(_ from: String). Types in Swift are, by default, non-Parseable (unsurprisingly, since I can't go add it to every type out there).
  • Some types are unconditionally Parseable. I just conform them directly to Parseable with e.g. extension Int: Parseable { }. Then if I have an Int, I can parse it.
  • Some types are conditionally Parseable. I might add extension Optional where Element: Parseable. Then, if I have an optional containing something that's Parseable, like an Int?, I can parse it. But if it contains something that's non-Parseable, you can't parse it.
  • Only types with generic parameters can be conditionally Parseable. You can't make an Int conditionally parseable – conditional on what?
  • If I am in some generic function and I have a generic type T, I do not know if it is Parseable. It might be. Maybe it's an Int. But since I do not know whether it is or not, the compiler won't let me us it's string initializer.
  • If a type doesn't support a protocol you can't pass it to a function that requires that protocol.
  • On the other hand, if I have a generic T and there is a where T: Parseable I can use it's init?(). I don't need to know what it is, just that I can use that.

Copyable is almost the same, except that part about types in Swift not being Copyable by default. Instead, that part is flipped around. This leads to some minor, but perfectly teachable, differences:

  • There's a protocol Copyable, which means you can copy a value. Types in Swift are, by default, Copyable. The compiler adds this conformance automatically.
  • Some types are unconditionally non-Copyable. I can opt them out of being Copyable by writing ~Copyable. So struct Box<Element>: ~Copyable cannot be copied.
  • Some types are conditionally Copyable – that is you can make them non-Copyable, then opt back in conditionally. I might add extension Optional where Wrapped: Copyable because when the type contains a copyable value, it's fine to copy it. So I can make copies of Int? but not copies of Box?.
  • Only types with generic parameters can be conditionally Copyable. You can't make an Int conditionally copyable – conditional on what?
  • If I am in some generic function and I have a generic type T, I know it will be Copyable. I do not know what it – but I know I can copy it.
  • Because Copyable is the default, unless a generic function explicitly says it won't make copies, it is assumed that it will.
  • You promise you won't make copies by using ~Copyable. The compiler will check you don't make any copies accidentally.
  • If I am in some generic function and I have a generic type T: ~Copyable, I don't know I can copy it, and the compiler won't let me. I might be able to. It might be an Int. But I don't know that, so I can't copy it.
  • If a generic function hasn't explicitly promised it doesn't rely on Copyable, you cannot call it with a non-Copyable type because you don't know it won't make a copy.
  • You can, of course, call functions that take non-Copyable types with Copyable types. You just know the function won't copy that type, despite it actually being Copyable.
7 Likes

I recall a physics professor once explaining how all physics classes had at least one "reset" where the students were told that everything before that point was "wrong" (or at least "incomplete") and that now they would talk about how things really work. :wink:

You could take that approach here as well: First say that struct MyType: ~Copyable means that struct MyType can't be copied. That's easy to explain, easy to understand, and correct enough for this example.

Then refine it: ~Copyable really means "not required to be copyable" or "maybe not copyable." Explain how that's really the same thing for simple declarations, but opens the possibility for more complex constructions: struct Pair<T: ~Copyable>: ~Copyable means that T "might not be copyable" (so the implementation of Pair cannot copy it) and the resultant Pair<Concrete> "isn't necessarily copyable" (unless some other constraint makes it so).

Ultimately, "copyability" is just like any other capability that can be added to (but not taken away) from a constructed type. The notational complexity comes from how Swift makes it available by default.

2 Likes

We could also make this the preferred spelling, so the type doesn’t itself state ~Copyable:

struct Pair<T: ~Copyable> {}
extension Pair: Copyable {}
4 Likes

It took me a long time to gain what little, tenuous grasp I have on this syntax as it is - and a big epiphany in that process was realising I should never think of "~" as meaning "not", because it's really just "maybe" (except when it isn't, of course, which is why this whole syntax and "opt-out" mechanism is so difficult).

I don't think lies to children is the right approach in this case. It instills a foundational way of thinking which isn't merely inaccurate (in the sense of Newtonian vs Einsteinian physics) but outright wrong. There's a big difference between learning refinements or edge cases (e.g. actually this doesn't work correctly at relativistic speeds, but this more complicated model does) vs having to start over.

This proposal is pretty clear that ~ should be read as “suppress” in type position going forward. It’s not the same as bitwise NOT (though it’s similar to how bitwise masking works), but neither is protocol composition with & the same as bitwise AND (though it’s still a form of intersection).

4 Likes

The way I think about it is that concrete syntax is useful in that it gives a way to talk about things, but it doesn’t itself carry any meaning. While there’s an analogy between negation and ~Copyable, and a protocol composition and bitwise and, it’s just that, an analogy. The “essence” of a protocol composition is the same today as it was in Swift 2 where “P & Q” was written as “protocol<P, Q>”.

4 Likes

I am really vibing with the concept of "maybe copyable" or "not necessarily copyable", and I like the syntax of ?Copyable for that.

My discomfort of reusing the same syntax for generic and non-generic uses is a direct echo of what I dislike about teaching what Foo&& means. I really wish the rule was "if you see token1, that's rvalue binding" and "if you see token2 (which is only allowed in generic contexts), that means forwarding binding".

I don't mean to rehash a conversation that has been had in this thread and the original one for ~Copyable, so I'll defer to the experts. Thanks for hearing me out and I'm enjoying the viewpoints. :slight_smile:

Reading “~Copyable” as anything other than “suppressing Copyable” is not a correct way to think about it.

The concept of ~Copyable is based on the principle of suppressing a default, implicit Copyable conformance or generic requirement that will otherwise exist in all of the places that you can write ~Copyable. That’s actually how it is implemented too.

If it helps, you can think of ~Copyable as stating the absence of Copyable.

2 Likes

It probably doesn’t help that we keep writing “noncopyable” in pitch and proposal titles. :joy:

13 Likes