Concise conditional Copyable conformances

In the discussion of Vector, I missed that Vector was conditionally Copyable because that was declared in an empty extension:

public struct Vector<let Count: Int, Element: ~Copyable>: ~Copyable {}

extension Vector: Copyable where Element: Copyable {}
extension Vector: BitwiseCopyable where Element: BitwiseCopyable {}
extension Vector: Sendable where Element: Sendable {}

In actual code, the primary declaration of Vector would be substantial, and the extensions would be even easier to miss. As a quality of life improvement for people reading the Swift interface, could we please have the ability to declare conditional conformances to constraints in the main declaration?

struct Vector<let Count: Int, Element: ~Copyable>: Copyable where Element: Copyable,
       BitwiseCopyable where Element: BitwiseCopyable,
       Sendable where Element: Sendable {
  /* ... */
}
1 Like

IMO this is less of a language concern and more of a tooling concern (IDE and documentation generation).

The reason why such conformances may appear easy to rewrite is because they either implement marker protocols or protocols with default implementations (either supplied or compiler-generated, such as Equatable/Hashable/Codable). As soon as you want to implement a protcol which doesn't fall under those constraints, the readability gets compromised much more, and e.g. associated types have no sound substitution:

public struct Vector<...>: Sequence where Element: Copyable {
    public typealias Iterator = // what?

    // so, does this type offer `makeIterator` unconditionally or not?
    public func makeIterator() -> Iterator
}

Needless to say, it's not uncommon to have extensions (even the "basic" ones that don't declare conformances) in totally separate files from the base type, so the status quo of method/conformance discoverability has been like this for Swift's whole lifetime basically.

But I totally see that an argument could be made that e.g. DocC should better help discover some common marker protocols, perhaps listing them much closer to the declaration.

struct S<T>: Sendable where T: Sendable {}

already compiles in Swift, declaring a structure where T is required to conform to Sendable.

I agree with nkbelov that listing conformances is a job for documentation, not source code.

1 Like

Isn't this changing type from being conditionally copyable to unconditionally copyable? And then there is no way to define such type with the suggested syntax?


In the review thread there was a long discussion on the matter, and seems like no better option has arise.

Not if you don’t cut out the where clause that immediately follows.

Sorry, I simply disagree with this sentiment. The language design should strive to be as self-documenting as possible. And Swift largely follows this maxim—for example, methods have argument labels so you don’t have to reference documentation to figure out what they mean.

3 Likes

That's correct: this already has a meaning in the language—

struct S<T>: Foo where T: Foo means that S unconditionally conforms to Foo and T is constrained unconditionally to conform to Foo; it does not mean that S<T> conforms to Foo conditionally when T conforms to Foo.

9 Likes

Ah, now I understand the point @vns and @1-877-547-7272 were making. Thank you for spelling it out further.

It’s also very close to where T: Foo, T: Bar, though not technically ambiguous with it (as long as you allow arbitrary lookahead). Maybe we should consider whether the idea is good before we get to the particular spelling.

…Though I’m not sure about that part either. The where clauses we have today are used for conformances in extensions, but they are also constraints that can be assumed within the body of the type/extension. If we have a shorthand for conditional conformances, those constraints presumably wouldn’t apply to the body, which is probably fine for Copyable but could be a little weird in other cases. (Imagine a type that conditionally conforms to Collection but unconditionally provides startIndex and endIndex. That might be desirable, but I wouldn’t want to do it by accident!)

3 Likes

I think it’s reasonable to restrict this flavor of sugar to conformances that can be synthesized by the compiler.

I haven’t thought deeply about the consequences of this idea on what logic can be embedded in the type system, but perhaps a workable version could be achieved by adding if clauses:

struct Vector<T: ~Copyable>: Copyable if T: Copyable {
    /* ... */
}

struct UnsafeBufferPointer<Element: ~Copyable>: ContiguousBytes,
       Copyable if T: Copyable {
    /* ... */
}

While updating the spelling to take advantage of the concise syntax, it would also be possible to clean up the primary declaration by moving the initial ~Copyable (and any other suppressed implied constraints) out of the angle brackets:

struct UnsafeBufferPointer<Element>: ContiguousBytes,
       Copyable if T: Copyable & Escapable,
       Collection if T: Copyable & T: Escapable,
       where T: ~Copyable, T: ~Escapable {
    /* ... */
}

Just a small nit: UnsafeBufferPointer is always copyable :sweat_smile:

The documentation says it only conforms to Copyable when Element conforms to Copyable and Escapable.

That documentation unfortunately is incorrect.

2 Likes

Conformance part at the bottom might be a bit confusing, but the declaration is actually defines that it always copyable:

struct UnsafeBufferPointer<Element: ~Copyable>

Note that there is no suppression of copyability for the whole type, and by default all types in Swift are implicitly copyable. The tricky part is Element: ~Copyable, but IIUC from the review discussion this actually means that Element isn’t required to be Copyable.

4 Likes

That's exactly right.

struct Foo<Element: ~Copyable> {...}
struct Foo<Element> where Element: ~Copyable {...}

are equivalent declarations which declare an always-copyable type Foo that accepts copyable as well as non-copyable Element types.

The docs have

@frozen
struct UnsafeBufferPointer<Element> where Element : ~Copyable

which is correct (but confusing) and they mean the UnsafeBufferPointer<Element> is always copyable and that it accepts Elements regardless of whether they're copyable. I'd write this as struct UnsafeBufferPointer<Element: ~Copyable>.

Maybe this helps:

// copyable types
struct IAmCopyableAndAcceptAnything<Element> where Element: ~Copyable {}
struct IAmAlsoCopyableAndAcceptAnything<Element: ~Copyable> {}
struct IAmCopyableAndAcceptOnlyCopyable<Element> {}

// non-copyable types
struct IAmNotCopyableAndAcceptAnything<Element>: ~Copyable where Element: ~Copyable {}
struct IAmAlsoNotCopyableAndAcceptAnything<Element: ~Copyable>: ~Copyable {}
struct IAmNotCopyableAndAcceptOnlyCopyable<Element>: ~Copyable {}

Note: I'm not trying to say this is obvious, it's a somewhat confusing notation that fits into Swift's syntax without breaking old code. The SE-0427 proposal explains it quite well.

7 Likes

There was a long discussion in the pitch thread and the first review about inferring Copyable suppression from ~Copyable generic parameters, eventually culminating here. Maybe DocC jumped the gun before a final decision was made?

And this is why I consult the Swift interface, because it is the authoritative source of a type’s declaration.

None of those changes Slava suggested were accepted, for better or worse. (not a contradiction) [Accepted] SE-0427: Noncopyable generics

1 Like