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

You'd still need to revert that somehow when needed:

struct Foo {
    // put something here to denote no copyability by default
}

extension Foo {
    // put something here to denote you want copyability back
}

To sum up here's a list of prefixes mentioned upthread (in no particular order):

~ - ! ?
not, maybe, discard, without, suppress dropimplicit, removeAssumedConstraint

Are we closer to the resolution now?

1 Like

I think @tera brings up a good point, not to mention that describing Self or Never would need type-system features we don’t currently have. It seems easier to deal with copyability by simply retaining/dropping the Copyable constraint.


[quote="tera, post:82, topic:63866”]
To sum up here's a list of prefixes mentioned upthread (in no particular order):

~ - ! ?
not, maybe, discard, without, suppress dropimplicit, removeAssumedConstraint

Are we closer to the resolution now?
[/quote]

I think not (and to a lesser degree discard and without) communicate the wrong idea of simply banning Copyable (not making it optional). dropimplicit and removeAssumedConstraint are arguably wordy.

On the other hand, I like maybe (though it's a bit vague); suppress hints at the implicit constraint but is also slightly vague. However, @taylorswift’s unknown is quite clear IMO.

1 Like

Joe is saying that it doesn't make sense to act like Copyable is a protocol because protocol conformances have certain properties, like being retroactively declarable, that do not make sense for Copyable.

3 Likes

I am confused. I thought this would be supported:

struct Foo: ~Copyable { // or whatever is the symbol we end up using
}

extension Foo: Copyable {}

No?

Indeed, the syntax you’ve referenced is supported, because that conformance is not retroactive (assuming that extension is in the same file as the original declaration).

Retroactive doesn’t just mean “not declared on the original declaration”, rather that the conformance is declared outside the original module (or in the case of layout constraints I believe @John_McCall said that it if it’s declared outside the original file it counts as retroactive).

3 Likes

So if I only can do this in the same module (or is it the same file in this case?) why would I want to write:

struct Foo: ~Copyable { // or whatever is the symbol we end up using
}
...
extension Foo: Copyable {}

instead of a simpler:

struct Foo {
}

I think you wouldn’t. Declaring the conformance via an extension in the same file is useful if it’s a conditional conformance, e.g.:

struct Optional <Wrapped> {

}

extension Optional: ~Copyable
    where Wrapped: ~Copyable { }
3 Likes

So you’re conceptualizing Copyable as a “type trait” rather than a Protocol because it needs the ability to disallow retroactive conformance via extensions. Rather than introducing a new conceptual entity of ‘type trait’ into the language could we not introduce the ability of protocols to disallow retroactive conformance?

Backing up a bit and taking a 30,000 ft view there’s a lot of complexity coming into the language. Looking at the variadic generics pitches, all of the ownership features, macros - it’s a lot. Each of the decision points taken in isolation are reasonable and defensible, but taken together it adds up.

So any time you have the ability to reuse a conceptual entity like Protocols instead of introducing a new one like TypeTrait I think it should be preferred.

Actually I think my code is wrong here, no?:

Shouldn't it be this instead?:

struct Optional <Wrapped: ~Copyable>: ~Copyable {

}

extension Optional: Copyable
    where Wrapped: Copyable { }

I realized while writing this that the struct Optional<Wrapped: ~Copyable> syntax seems necessary for the Optional use case, which reminded me that the proposal is explicit that (at least at first) noncopyable types cannot be used as type parameters for generic types, which means that it would initially be impossible to express the conditional conformance in my example, right?

3 Likes

Correct, this proposal doesn't include any way to express this with any syntax—all of that is left for future work.

This is probably a bit wild with a lot of edge cases, but I'm going to suggest it anyway because I fear those ~Copyable markers are going to be quite viral in many places and I'd like to reduce the need for them. It goes beyond the scope of the current proposal, trying to anticipate future needs.


I suppose we could make those anti-Copyable markers go away (for types) in many instances by redefining things like this:

struct Thing { var i: Int } // not Copyable by itself

// but this is implicitly inferred because all the members are copyable:
extension Thing: Copyable {}

And if you have this:

struct Thing<Wrapped> { var w: Wrapped } // not Copyable by itself

// but this is implicitly inferred based on the types of the members:
extension Thing: Copyable where Wrapped: Copyable { }

And if you aren't satisfied by the implicit inference (maybe you're writing a copy-on-write container of some sort), you can just write the conformance differently to suppress the inferred one:

struct COWThing<Wrapped> {
   var s: Storage<Wrapped>
   mutating func replace(with new: Wrapped) {
      if isKnownUniquelyReferenced(&s) {
        s.content = new
      } else if Wrapped is Copyable {
        s = Storage(s.content, new) // copy here!
      } else {
        fatalError("Logic error: should always be uniquely referenced when Wrapped is not Copyable.")
      }
   }
}

// manual conformance to Copyable: limits it to when Wrapped is copyable
// to only allow copies when copy-on-write is implementable:
extension COWThing: Copyable where Wrapped: Copyable { }

// the manual conformance replaces the one that would be inferred.

And for types which are never copyable, you can provide a deinit to make them unconditionally uncopyable:

struct FileHandle {
   var handle: Int
   deinit { ... } // supress Copyable
}

No need for ~Copyable to define all these types.

Of course, you'll still need ~Copyable (or some equivalent) for generic functions. But maybe not all of them. For instance we now have this:

func test<T>(_ t: T) -> T
where T: Copyable // this line is inferred
{
   return t
}

But I suppose we could make borrow and consume automatically remove this inference, and thus reduce the need for ~Copyable:

func test<T>(_ t: consume T) -> T
// no inference that T is Copyable because of `consume`
{
   return t
}

Note that while borrow and consume suppress the inference, you can still write it explicitly if needed:

func test<T: Copyable>(_ t: borrow T) -> T {
   globalVariable = t // copy here!
   return t
}

This is probably going to break existing code that makes use of borrow and consume though. Has this feature shipped yet? Or can it be changed for Swift 6?


There's obviously going to be some situations where ~Copyable is still needed. But not having to write ~Copyable in most places would make it much better for working with non-copyable types.

1 Like

It may not be stated explicitly in the proposal, but it’s clear from the historical discussion of this feature/proposal that default-implicit copyability is not on the table. Without it, the default experience is akin to Rust, which is not ergonomic.

1 Like

That's not really what I'm going for either. Perhaps I should have spent more time making my post clearer, but the idea is that unless you write a copyable conformance manually yourself, the copyable conformance is implicit (and automatically conditional based on the members). That's why I always wrote the Copyable conformance (implicit or not) as an extension in my examples instead of the main type declaration: so it can be expressed as a conditional and can be added explicitly as an extension (presumably in the same file only).

Right, while I agree with @ksluder in the strictest sense (we wouldn't want to start having any types be noncopyable by default) I think that in the fullness of time it would be reasonable to consider various conditions under which the Copyable constraint would end up suppressed without writing out ~Copyable explicitly. A conditional Copyable constraint seems quite suitable as such a mechanism.

However, I also don't think we need to worry about that right now. It might be more germane in a proposal that fleshes out more of the generics story and people would actually be able to write code like @Torust has posted above:

but for now I'm perfectly happy introducing noncopyable types without any mechanism by which the Copyable constraint suppression would be assumed.

4 Likes

Personally, I don't think we would ever want to implicitly suppress the Copyable conformance for public types, and I'm not sure it's a good idea for private or internal types. Making a type non-copyable is a major change that will require work in almost every function that touches it, and we don't want to push that lightly on people.

I think some people will find non-copyability to be viral, but for the most part I don't think they will be — in fact, I think we specifically don't want them to be such a core part of most programmer's experience with the language.

8 Likes

I believe proposed ~Copyable expression has weak googleability. I prefer keywords since they are more readable and googleable.

4 Likes

As one of the people who’s commented on the potential virality of move, I’d like to clarify that I can see myself explicitly moving an otherwise copyable type far more often than I would write non-copyable types.

2 Likes

I read code far more often than I Google language keywords. And any intro blog (or TSPL chapter) on this topic will probably cover both versions of the constraint.

1 Like

If we used a keyword, it would probably be a common word and so would still have poor searchability.

3 Likes

I don't have a lot of experience with C++, but I found ~Copyable (remark for destructor) very intuitive, and it's a clear marker for me that there is a deinit on struct or enum and I think it's a very good idea.