Is ~Copyable confusing?

Is ~Copyable confusing?

I refrained from asking the question here: What is ~Copyable for?

Here it is in a nutshell why I ask this question.

@main
enum Driver {
    static func main() {
        let bar = Bar (u: 2)  // Error: `bar' consumed more than once
        let fooBar = Foo (bar)
        let u = bar
        
        let jar = Jar (u: 3)   // No Error: `jar' consumed more than once
        let fooJar = Foo (jar) // What? But, Jar is copyable!
        let v = jar
    }
}

struct Foo <T: ~Copyable>: ~Copyable {
    private var u: T
    
    init (_ u: consuming T) {
        self.u = u
    }
}

struct Bar: ~Copyable {
    let u: Int
}

struct Jar {
    let u: Int
}

What is the purpose of ~Copyable qualifying the generic parameter? I ask because not only can I pass a non-copyable value, I can also pass a copyable value.

1 Like

In this case T may be either a copyable or noncopyable type. <T: ~Copyable> simply suppresses the implicit Copyable conformance.

See also: Copyable and Implicit Conformance to a Protocol.

From the Copyable doc:

To suppress an implicit conformance to Copyable you write ~Copyable. For example, only copyable types can conform to MyProtocol in the example above, but both copyable and noncopyable types can conform NoRequirements in the example below:

protocol NoRequirements: ~Copyable { }
3 Likes

~Copyable should be read as "maybe-copyable", not "non-copyable".

For the purposes of the following discussion, let's take for granted that we should have noncopyable types in Swift; consider then how to add them.

First, every existing type is considered to conform to a new protocol, Copyable, even if they don't state : Copyable.

Then, every existing generic parameter is also considered to conform to Copyable even if it doesn't state it; if you don't do this then existing code like this won't compile any more:

func f<T>(_ t: T) {
    print(t)
    print(t)
}

So then, what syntax should be used to introduce a non-copyable type? Fortunately, Rust had tread these waters already, and Swift just followed Rust's solution: T: ~Copyable means "stop assuming the backward-compatible behavior that T: Copyable".

That makes sense of the generic parameter: <T: ~Copyable> means "this code may not assume T is Copyable", it doesn't mean that T isn't Copyable.

It also makes sense (though I have to squint a bit more) of the annotation on a type; struct S: ~Copyable means "this code may not assume that S is Copyable". You might ask, doesn't that mean that S isn't Copyable? But consider this case:

struct S<T: ~Copyable>: ~Copyable {
    var t: T
}
extension S: Copyable where T: Copyable {}

Meaning, "S is Copyable if and only if T is Copyable". Now S is sometimes copyable. So in all cases, reading ~Copyable as "maybe-copyable" will help your understanding.

The original question was, "is this confusing". And I think the answer is, yes, it is confusing. But I also think that it's the only reasonable solution to the problem given the constraint of "we do want noncopyable types in Swift". Whether you believe that precondition, I'll leave up to you


12 Likes

In terms of set theory, it’s that Copyable and ~Copyable types are not disjunct sets. In fact, Copyable is a subset of ~Copyable. That means that every type that can be copied can also be treated like it wasn’t. Yet the other direction is not possible.

Perhaps that makes a bit clearer what the meaning of ~ really is. It doesn’t mean “opposite” but “superset”.

3 Likes

I liked your step-by-step approach


But it does mean T isn’t Copyable, unless the context permits it:

struct S<T: ~Copyable> {
  let t: T
  // error: Stored property 't' of 'Copyable'-conforming generic struct 'S' 
  //        has non-Copyable type 'T   
}

So (somewhat ironically) only if S: ~Copyable could T be a Copyable type.

I like your maybe Copyable but I think of ~Copyable more as actively accommodating either Copyable or non-Copyable types; “maybe” sounds ambiguous, and “might be” doesn’t really indicate that there are consequences. ~Copyable is more like being amphibious, where one has different capabilities/constraints for the different contexts.

For me the key thing is to realize these are type constraints. The type constraints we see most ofter are is-a relationships, which we happily think of in terms of sets or inheritance. But ~Copyable, ~Escapable, and protocol inheritance are not isa relationships.

1 Like

Read T: ~Copyable as "T is not constrained to be Copyable" rather than "T is constrained to be not Copyable." (In other words, consider this to be an instance where a programming language is employing the rhetorical device known as hypallage.)

If T is not constrained to be Copyable, you cannot do things that rely on its copyability in generic code.

(Note that Swift does not support, for any protocol P, the "negative constraint" where a generic T must not conform to P: you can either positively constrain T to conform to P or not positively constrain T to conform to P. It's just that, for some protocols, the default is that there is an implied constraint, so there must be some way to spell the absence of it.)

8 Likes

I think what can get one's brain tied in knots is that users of generic code and authors of generic code, the 'usability' of generic values is in a dual relationship. As a user, the fewer constraints there are, the more things you're able to pass in to the generic context. But as an author, the constraints are precisely what let you do things with the values passed in. And as you say, ~ in this context should be read as removing the constraint. Which means that authors of generic code accepting a ~Copyable parameter cannot assume that the value is able to be copied. But a type which can be copied doesn't have to be copied, so you're free to pass in Copyable values—you just know that their 'copyability' isn't material to their internal use!

4 Likes

On the question Is ~Copyable confusing? I would point out that you just explained that “removing the constraint” adds requirements for the implementation (implicitly by operation of another default). So I suggest that presenting ~Copyable as removing a constraint or undoing the default is confusing because that has to be backstopped with further logic explaining further requirements (and seemingly contrary error messages). To me the continuing confusion suggests explanations in terms of set theory and negative defaults have not been effective.

The explanation that makes sense to me is to not use any form of is-a but always describe it as a type constraint, to be amphibious (or accommodating): able to handle both, in the historical context where the default is Copyable (just as animals once lived only in the sea). Further putting it in the category of other non-is-a type constraints helps me realize that there are a few Swift features that justify this extra care.

1 Like

I'm not sure I quite understand what you're saying regarding "adds requirements"—removing a constraint removes requirements on the generic parameter, which means there are fewer things that the author of generic code can do.

IMO understanding ~Copyable as removing a constraint is most elucidating because the relationship between including ~Copyable and omitting ~Copyable in a generic parameter's constraints is entirely analogous to omitting P and including P in a generic parameter's constraints respectively. I

If P provides a requirement foo(), then when a generic type T is constrained to P, you are free to call t.foo(). If P is removed as a constraint, one cannot call t.foo() because such a method may not be available on the value passed in. Similarly, if one constrains a generic type T to Copyable (by declining to write ~Copyable), they are free to copy t: T as they see fit. But if they remove the Copyable constraint (by writing ~Copyable), the implementation may no longer assume t can be copied, because the copy operation may not be available for the value.

4 Likes

What I find challenging is that ~Copyable has different meanings in your short code sample (even in a single line!).

In the declaration for Bar, the ~Copyable means "this type is noncopyable." All the rules of noncopyability apply to Bar, extensions to Bar, its instances, etc.

struct Bar: ~Copyable {
    let u: Int
}

However, the ~Copyable constraint on T in your declaration for Foo means "this type is allowed to be noncopyable, but may instead be copyable." You're allowed to create instances of Foo<Bar> (which you wouldn't without this constraint), or using copyable types like Foo<Int>.

struct Foo <T: ~Copyable>: ~Copyable {
    // ...
}

Note that the ~Copyable constraint on Foo itself is a little different from both of these – like with Bar, it specifies that the type is noncopyable, but at the same time allows copyability with a tighter constraint:

extension Foo: Copyable where T: Copyable {}
6 Likes

What would the situation be in a language where "default" is flipped, ~Copyable not needed, instead a bare <T> would imply Swift's <T: ~Copyable>, and you'd need to put an explicit <T: Copyable> to get to Swift's default behaviour. What are the downsides?

1 Like

I believe that language is called Rust :)

7 Likes

The downside, at least as the core team has expressed it, is that nearly every type will have to include an explicit conformance to Copyable, and almost as many extensions & protocols will need a Copyable requirement.

1 Like

Though it’s syntactically more confusing, I don’t think that it’s conceptually more difficult than any other more ‘normal’ constraint. It’s perhaps elucidating to compare to something like Equatable where the constraint is not explicit but the implementation can be.

In the ‘positive constraint’ direction, the analogous statement would be to say that leaving an Equatable conformance off of a concrete type struct S has a different meaning from leaving an Equatable constraint off of a generic parameter T. I think this is a defensible statement, but only insofar as it draws out the difference between concrete types and generic parameters in general—namely that generic parameters may, at runtime, be satisfied for any type which satisfies the constraints.

It would be just as correct, IMO, to say that a declaration struct S: Equatable means something different than <T: Equtable> or that struct S (implicitly Copyable) means something different than <T> (implicitly Copyable).

The same goes for conditional conformances—the absence of an unconditional Equatable constraint on the base declaration (analogous to a ~Copyable negative constraint) does not imply that the type cannot be Equatable (or Copyable)—it’s perfectly reasonable for an extension to introduce a conditional constraint which allows the conformance to be satisfied. Typically such constraint takes the form of a member type itself being constrained to be Equatable (or Copyable)!

I really do think the mental gymnastics around this become much easier to reason about if you think in terms of ‘constraints’ and ‘conformances’ as two sides of the same coin.

3 Likes

This language is called Rust, and it results in a lot of things needing to state <T: Clone> (and because cloning is never implicit, a lot of generic code needing to explicitly call .clone()). Cloneability is not something Rust uses the ~ conformance suppression symbol for.

1 Like

I think this is conceptually wrong, as I said in my first response to the thread. struct Bar: ~Copyable means that this declaration of Bar does not imply that it's Copyable. Copyability can still be added by an extension.

Also to nitpick,

extensions to Bar

Extensions do not inherit the suppression by default; extension Bar { ... } is equivalent to extension Bar where Self: Copyable { ... } (for backward compatibility's sake). If you want to write an extension to Bar that applies when Bar is not Copyable, you have to be explicit:

extension Bar where Self: ~Copyable {
}
2 Likes

We only infer default Copyable (and Escapable) constraints on generic parameters in extensions. In this example, Bar has no generic type parameters, so nothing is inferred in the extension and no Self: ~Copyable is needed (nor permitted I believe).

Had Bar been a protocol, this extension and reasoning would be correct. Self in a protocol is a generic parameter (in fact the only one) and thus gets the Copyable inferred for Self in extensions unless suppressed.

4 Likes

another point in favor of "yes" XD

1 Like

I didn't know this, but I think it's beside the point. A non-generic type like Bar can be either copyable or noncopyable, not "copyable under certain conditions" like you get with protocols and generic types and functions.

:thought_balloon:
Personally speaking, the concept of ~Copyable is not confusing, but the appearance of ~Copyable is confusing.
~ is a bit-flipping (bitwise NOT) operator in C. I mean ~i is the complement of i in C.
I did indeed think ~Copyable was the complement of Copyable at first.

1 Like