[Second Review] SE-0427: Noncopyable Generics

Hello, Swift community!

The second review of SE-0427: Noncopyable Generics begins now and runs through July 10th, 2024.

This is a focused second review regarding an amendment to the original proposal. The amendment subsets out the ability to mark associated types on protocols as ~Copyable, instead describing this feature as a future direction.

The diff for the amendment to the proposal can be found here.

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 me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0427" in the subject line.

Trying it out

If you'd like to try this proposal out, you can download a toolchain supporting it for Linux, Windows, or macOS using the latest development snapshot from main or the Swift 6 release branch. You can also try it in the Swift compiler included in seeds of Xcode 16.

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 here.

Thank you,

Ben Cohen
Review Manager (taking over from Holly Borla for this proposal)

10 Likes

To help seed this discussion, consider these two extensions:

protocol Cancellable {}
struct Scheduler<Job: ~Copyable>: ~Copyable { /*...*/ }

extension Scheduler: Cancellable { }
extension Scheduler: Copyable where Job: Copyable {}

Both are conditional on Job: Copyable, but only the Copyable extension is required to have the where-clause written explicitly. That's because this will be an error, because it tries to make the type unconditionally Copyable in an extension:

extension Scheduler: Copyable {}
// `- error: generic struct 'Scheduler' required to be 'Copyable' but is marked with '~Copyable'

Looking towards the future where we have a ~Escapable, we will end up with this situation:

extension Optional: Copyable where Wrapped: Copyable {}
extension Optional: Escapable where Wrapped: Escapable {}
extension Optional: Sendable where Wrapped: ~Copyable & ~Escapable & Sendable {}
extension Optional: BitwiseCopyable where Wrapped: BitwiseCopyable { }

Notice that we do not have to explicitly opt-out of Escapable on the Wrapped parameter for a conditional Copyable conformance, yet we need to do so for the Sendable conformance so that the conformance only requires Sendable.

I'll also note that this has already been illegal in the proposal, for any protocol P that is not Copyable:

extension Scheduler: Copyable where Job: P {}
// `- error: conditional conformance to suppressible protocol 'Copyable' cannot depend on 'Job: P'

This is greatly bothering me, as it feels like we somehow ended up with rules that are opposite to what the LSG asked for in their review notes:

  • Inference of Copyable requirements on conditional conformances to Copyable is confusing; requirements in this case should be made explicit.

The proposal was updated to reflect this:

We do this by declaring a conditional conformance:

extension List: Copyable where T: Copyable {}

Note that the where clause needs to be written, because a conformance to
Copyable declared in an extension does not automatically add any other
requirements, unlike other extensions.

This is all very good. The rules as described make for eminently readable code, as we have to explicitly spell out what we mean. These variants are both hard errors:

extension Optional: Copyable where Wrapped: ~Copyable {}
// error: cannot suppress '~Copyable' on generic parameter 'Wrapped' defined in outer scope
// error: generic enum 'Optional' required to be 'Copyable' but is marked with '~Copyable'
// error: associated value 'some' of 'Copyable'-conforming generic enum 'Optional' has non-Copyable type 'Wrapped'

extension Optional: Copyable {}
// error: generic enum 'Optional' required to be 'Copyable' but is marked with '~Copyable'
// error: associated value 'some' of 'Copyable'-conforming generic enum 'Optional' has non-Copyable type 'Wrapped'

All seems well on the release/6.0 branch. We only have a single one of these suppressible quasi-protocols (Copyable), and there appears to be only one way to spell conditional conformances to them, by explicitly spelling things out. We don't need to change anything there, and I think SE-0427 can be considered fully implemented there.


However, on main, we also (provisionally) have a second suppressible quasi-protocol, Escapable. I think the addition of this second protocol has uncovered an inconsistency in the current implementation, and this will need to be fixed. (On main only, as it only matters there.)

When I go and relax the escapability requirement on Optional, the meaning of the conditional Copyable conformance syntax suddenly changes in an unexpected way:

enum Optional<Wrapped: ~Copyable & ~Escapable>: 
  ~Copyable, ~Escapable 
{ ... }

extension Optional: Copyable 
where Wrapped: Copyable {}

extension Optional: Sendable 
where Wrapped: Sendable & ~Copyable {}

I've grown to assume that for generic type parameters, the omission of an explicit requirement on one of these suppressible protocols means an implicit positive conformance -- so I was very much surprised to find that their actual meaning is:

extension Optional: Copyable 
where Wrapped: Copyable & ~Escapable {} // !?

extension Optional: Sendable 
where Wrapped: Sendable & ~Copyable & Escapable  {} // OK!

Additionally, I cannot even avoid the confusion by spelling out precisely what I mean, as the above code produces the following error on the Copyable conformance declaration:

error: cannot suppress '~Escapable' on generic parameter 'Wrapped' defined in outer scope

I think this goes directly against the spirit of the LSG's request -- the review notes ask that "requirements in this case should be made explicit", but here we currently force them to remain implicit (and we also use a default that's inconsistent with the regular case).

I'd very strongly prefer if we required all conditional conformances to a suppressible protocol to explicitly spell out what their requirements are for all such protocols that the type suppressed on its original declaration.

extension Optional: Copyable where Wrapped: Copyable {}
// error: Hey dummy, you forgot to specify what you want for `Escapable`

extension Optional: Copyable where Wrapped: Copyable & ~Escapable {}
// Should be OK

extension Optional: Copyable where Wrapped: Copyable & Escapable {}
// Should also be OK

(The diagnostic would also make sense to be a warning rather than a hard error, although there is no source compatibility concern here. Ideally it would come with fix-its to help resolving it.)

This does not need to affect SE-0427, as we only have Copyable there; but I think it should be part of the ~Escapable proposal -- the implicit default choice is misleading/confusing in this context, and we should rather be forcing developers to make an explicit choice.

7 Likes

I did forget that BitwiseCopyable exists in 6.0 and it is also somewhat suppressible.

Luckily(?), suppressing it has very different meaning than suppressing Copyable. So release/6.0 appears to be still okay, I think. :face_with_spiral_eyes:

struct Box<Item: ~Copyable>: ~Copyable, ~BitwiseCopyable {
    var item: Item
}
extension Box: Copyable where Item: Copyable {}
// OK; Item is **not** required to be BitwiseCopyable, 
// but we don't need (or want) to mention this.

extension Box: BitwiseCopyable where Item: BitwiseCopyable {}
// error: cannot both conform to and suppress conformance to 'BitwiseCopyable'

Removing the ~BitwiseCopyable declaration from Box lets the second conditional conformance build. In that case, Item will be assumed Copyable, as BitwiseCopyable is (implicitly :melting_face:) refining it.

These suppression semantics are quite weird, but I suppose it does make sense to treat BitwiseCopyable as a distinct case because its suppression has rules that are very distinct from Copyable/Escapable. (Edit: the more important distinction is probably that BitwiseCopyable isn’t universally assumed by default on every type, while Copyable/Escapable are like that.)

It's also fine that the review notes do not mention it: unlike Copyable and Escapable, we probably would not want to force developers to explicitly spell out whether they want bitwise copyability. It would be quite silly to require a ~BitwiseCopyable clause here:

extension Box: Copyable where Item: Copyable & ~BitwiseCopyable {} // What?

Swift appears to be turning a tiny bit eccentric as it grows up.

1 Like

I’m not up on the current state of the Escapable pitch, but this would break source compatibility if Optional suppressed Copyable in 6.0, somebody wrote an extension, and then suppressed Escapable in 6.1. I think your original interpretation is the only workable one across modules packages, and I’m not sure what’s going on on main that makes it not what actually happens.

2 Likes

While I don't feel too strongly about it, I think in hindsight, the original design had a consistency and logic to it, which the two proposed revisions only serve to obscure:

  1. Associated types are not like generic parameters, and while I might be wrong, I suspect there is no simple way to give associated types a "default to Copyable" behavior that resembles how generic parameters default to Copyable. It's fine to subset it out, but I suspect we will end up with ~Copyable associated types in their current form anyway.

  2. The where clause behavior for a conditional conformance to Copyable is appealing, but I worry that it's still the wrong tradeoff, because it introduces another special case without fundamentally absolving the user of having to learn about the rule for default Copyability in extensions anyway.

6 Likes

A conformance to Copyable or Escapable must appear in the same module as the nominal type though, so regardless of how the where clause interpretation vs. conditional conformance thing shakes out, the original author of the nominal type has to decide to opt-in to Escapable, and they can tweak their extension where clauses (or not) as needed.

2 Likes

~BitwiseCopyable can only appear on a nominal type declaration, where it means "do not synthesize the automatic conformance". (We could also do something similar for ~Sendable, which is also derived for non-public types today.) BitwiseCopyable also refines Copyable. All other behavior falls out from those two rules.

In the "language spec", there isn't one definition of ~ that everything is a special case of; ~Copyable and ~BitwiseCopyable are just distinct concepts.

1 Like