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

Theoretically, we could also use this syntax (in the future) to suppress automatic Equatable from bare enums? I've wanted that ability occasionally in the past.

Anyways, I like the xCopyable form much more than the previously pitched attribute. I'm ambivalent to ~Copyable or -Copyable spellings.

5 Likes

An idea occurred to me and I’m going to share it, but I want to acknowledge that I have only a cursory knowledge of this problem space, and so maybe my idea simply reflects a misunderstanding of what we’re dealing with. Anyway, for what it’s worth:

Regarding the subtraction of the usually implicit Copyable behavior - I want to ask if it is possible/has it been considered that we could approach it in the additive direction (like we do with most things)? I.e., expose everything that a struct is except for copyability under a new keyword that is a sibling of struct (e.g., noncopyablestruct), and then reimplement struct under the hood to simply extend the mechanics of noncopyablestruct that last copyable mile. Anyone who has this niche need can use the lesser-known, possibly more verbose keyword (noncopyablestruct).

Edit:
Another direction of syntax options: struct(-copyable) and enum(-copyable)

Yes, I think suppressing that kind of implicit conformance for other protocols would be a very natural future direction for this syntax (whatever that syntax ends up being).

8 Likes

And, in fact, this is explicitly discussed under Alternatives Considered ("Suppressing implicitly derived conformances with ~Constraint").

5 Likes

I don't have much to add here at this point in the review. I vote for spelling the constraint as !Copyable since ! is well-understood to mean "not" in Swift and other squiggly-bracket languages.

Edit: I prefer ! over ~ because I read the latter as "bitwise not". I could be convinced to vote for -Copyable (which I read as "minus Copyable".)

1 Like

It seems that at this stage people are voting for what the x they like it to be so for me ~ stands out better then – and my vote goes for ~. Also because it reminds me of negation or logical complement. Most of all I hope that the language team will choose what they feel is the most appropriate x here.

1 Like

I'm concerned that the reading as "not" is specifically misleading, since in its generalization, that's not strictly what it means; it's more like "this specific declaration does not promise Copyable". In the fullness of time, we want to allow for conditionally copyable types, which would look like:

enum Optional<Wrapped: ~Copyable>: ~Copyable { ... }

extension Optional: Copyable where Wrapped: Copyable { ... }

so in the original declaration, Wrapped and Optional aren't "not copyable", they are "not assumed to be copyable at this point". They could be copyable elsewhere.

9 Likes

Is it necessarily a good idea to use the same syntax for suppressing implicit Copyable conformance and for removing the implicit Copyable constraint? How then would one spell the “must not be Copyable” constraint?

Keeping the syntaxes distinct seems much clearer:

Spelling Meaning
struct Foo: ~Copyable values of Foo are move-only
enum Optional<Wrapped: maybe Copyable> Wrapped may be a copyable or move-only type
struct NumberEater<Num: BinaryInteger & ~Copyable> Num must be a move-only integer type

How does the optionality of ~Copyable intersect with overload resolution? If Swift eventually gains explicit specialization, can func f<T>() { } be specialized for T: Copyable and T: ~Copyable separately?

2 Likes

Frankly, I do not see a great deal of difference between ~ and not in this regard. If ~ (the logical negation symbol) can mean "not copyable at this point – but could be copyable elsewhere", so can mean "not Copyable", isn't it?

A valid question indeed if we want to have something that shouldn't be copyable at all.
"never Copyable" ?

1 Like

This proposal doesn't try to solve that problem, and personally, I don't think it needs to be, but I could be convinced otherwise. Do you have a use case for this?

-Joe

2 Likes

Currently I find all of ?, ~, and -, acceptable.

However, one additional thing that could be a future direction of the ~Copyable syntax is conditionally used conformances: e.g:
print currently takes Any and internally does dynamic casts value as CustomStringConvertible, etc, but there could be another print func print(_: (some ~CustomStringConvertible)..., , separator: String = " ", terminator: String = "\n") which would use the given CustomStringConvertible conformance if available, and would continue to dynamically cast otherwise.

1 Like

The concept of ~Constraint is that you're peeling away bits that are assumed about a type, directly on the declaration, to give you the most minimally capable version of the type that you can later extend.

So, the proposal mentions this as a "not allowed yet" thing, but I think ~Constraint cannot in general be written on an extension because it doesn't make sense conceptually. Extensions don't remove functionality or capabilities, they can only add them. Copying is a capability, so adding it back after suppressing or removing the assumption of it via ~ (or whatever syntax) can make sense.

Before diving into this, I want to note that almost everything below are forward-looking statements and not currently proposed functionality.

Let's start with an example that also has methods and data:

struct GenericBox<T: ~Copyable>: ~Copyable {
  var data: T
  func withLoan(_ f: (borrowing Self) -> Void) { f(self) }
}

struct ConcreteBox: ~Copyable { 
  var data: Int
  func withLoan(_ f: (borrowing Self) -> Void) { f(self) }
}

So far, we have two noncopyable types that support a withLoan method. That method works whether the type is copyable or not, and that's important to note. We cannot have the existence of an extension cause other functionality of the type to now be invalid. So you couldn't write a getCopy method that returns a copy of self in that GenericBox definition, because it does not assume the ability to copy. But the future goal is that you could write a conditional conformance to Copyable that has such a method, like this:

extension GenericBox: Copyable where T: Copyable {
  func getCopy() -> Self { return self }
  // other things only available for copyable instances of the box ...
}

extension GenericBox {
  // more methods that work with copyable and noncopyable boxes ...
}

Here we have two extensions. One adds the ability for some GenericBoxes to be Copyable and extra functionality only for those Copyable boxes. The other extension adds functionality for all boxes, copyable or not. Notice how that side-steps the idea of "not Copyable" in the type system, i.e., you don't write "extension of GenericBox when it is not copyable" and add methods that only work for noncopyable boxes. Instead, you're extending a box that never had the capability to copy. Everything a noncopyable box can do, a copyable box can do as well.

It's OK if you add a capability unconditionally after having suppressed its implicit conformance. The proposal cites an example of adding Equatable back after implicitly suppressing it for an enum. That's probably not very controversial since the extension adds the missing functionality to make it work. Like for Sendable, when you add Copyable in an extension, there are some rules:

struct ConcreteBox: ~Copyable {  
  var data: Int
  // .. other things ..
}

extension ConcreteBox: Copyable {
  func getCopy() -> Self { return self }
}

An extension adding Copyable can only technically work in the same translation unit (and really ought to be limited to just the same file) since all of the data stored within it must be known to the compiler. That's just a property of Copyable being a sort of data layout constraint like Sendable.

Otherwise, if you read the above strictly as "ConcreteBox suppresses Copyable" ... "extension of ConcreteBox where all instances are Copyable" then it does seem a bit silly to have suppressed Copyable initially, since it doesn't have any explicit requirements that you can group up under this extension as an organizational tool to show readers "here is how it conforms".

So the unconditional Copyable after initially ~Copyable is something the compiler could warn about as being silly. But I don't necessarily think it's a problem, since it's already impossible to write this extension outside of the same module / file the type is defined within.

6 Likes

Right. By “scale” here I’m meaning “scale to multiple declarations or generic arguments”, since ~Copyable must be applied individually to each type, whereas something like a scoped @begin(suppressImplicit(Copyable)) or a @suppressImplicit(Copyable) on a generic function would apply to all declarations within that scope and all generic arguments to that function respectively. It’s less about the number of characters and more about the repeated ~Copyable visual noise.

EDIT: to give another example, I prefer

@suppressImplicit(Copyable)
struct SomeStruct<A, B> {}

extension SomeStruct: Copyable where A: Copyable, B: Copyable {}

to:

struct SomeStruct<A: ~Copyable, B: ~Copyable>: ~Copyable {}

extension SomeStruct: Copyable where A: Copyable, B: Copyable {}

The @begin(suppressImplicit(Copyable)) does veer close to creating a “local language dialect” – to me that’s something desirable (since I want the compiler to warn me if I’m not being explicit about ownership in certain contexts) but I do also see the potential downsides.

Just to give my take on how this could work: @suppressImplicit(Copyable) on a class or actor would warn if there are no generic arguments but would otherwise have no effect; @suppressImplicit(Copyable) would not apply to nested declarations and you’d have to use @begin(suppressImplicit(Copyable)) for that.

1 Like

We've also tried to steer clear of that kind of #pragma-like scope. I think it would be reasonable to add a flag that warned about types / generic parameters that didn't explicitly opt in/out of copyability, though.

I think that's a fair guess about what it'd look like under this proposal to declare something like Array or Dictionary that wants to support non-copyable elements.

3 Likes

As of today, a type cannot have more than one conformance for a specific protocol, even with different conditional bounds. e.g.:

struct Foo<Bar> {
    var bar: Bar
}

extension Foo: CustomStringConvertible {
    var description: String { "Foo\(bar)" }
}

// error: Conflicting conformance of 'Foo<Bar>' to protocol 'CustomStringConvertible'; there cannot be more than one conformance, even with different conditional bounds
extension Foo: CustomStringConvertible where Bar: CustomStringConvertible {
    var description: String { "Foo\(bar.description)" }
}

If we do not plan to lift this restriction, we could say that

struct Foo<Bar> where Bar: Copyable {}

implicitly also means that Foo is not Copyable if Bar is not Copyable i.e.

extension Foo: ~Copyable where Bar: ~Copyable {}

as unconditional Copyable conformance would result in duplicate conformance and is not support by Swift.

Therefore one doesn't need to write:

extension Foo: ~Copyable {}
extension Foo: Copyable where Bar: Copyable {}

as the second line implies the first line.

Therefore we could change the meaning of : ~Copyable to mean that the type does not conform to Copyable. If one wants to define that Foo may conforms to Copyable they need to remove : ~Copyable and add the conditional conformance.

This at least makes it clearer for type definitions but we still have the problem with generic functions where func foo<Bar: ~Copyable>(bar: Bar) would still mean that it may conform to Copyable.

That's not quite right. Foo<Bar> might want to be copyable when Bar is copyable, but it might also want to be copyable always (maybe Bar is boxed and shared) or never (maybe Foo is enforcing additional constraints). That doesn't have to be communicated via extension, but all three possibilities have to have some syntax.

1 Like

I think we can express all possibilities:

// implicitly always `Copyable` 
struct Foo<Bar>{}
// explicitly always `Copyable` 
struct Foo<Bar>: Copyable{}
// never `Copyable` 
struct Foo<Bar>: ~Copyable{}
// only `Copyable` if `Bar` is Copyable
struct Foo<Bar: Copyable>: Copyable{}

Do I miss something?

How do I express that Bar is maybe-Copyable but Foo is always Copyable?

Could the spelling therefore be

struct S: without Copyable {
}

I think words are clearer and easier to find information about than sigils. This would also fit with the proposed parameter packs syntax.

12 Likes

hm... it could work with e.g.:

struct Foo<Bar: ~Copyable>: Copyable {}

but now ~Copyable again means that it may conform to copyable and not never.

However, may main point is still that we should imply that conditional conformance to Copyable removes the implicit Copyable conformance so this is not necessary:

struct Foo<Bar> {}
extension Foo: ~Copyable {} // this should be implied by the conditional conformance below and should not be necessary 
extension Foo: Copyable where Foo: Copyable {}

Sendable actually works like that today:

struct Foo<Bar> {}
// Foo is now only Sendable if Bar is Sendable as well, without that Foo is always implicitly Sendable, even if `Bar` is not `Sendable` as `Bar` is not a stored property.
extension Foo: Sendable where Bar: Sendable {}
3 Likes