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

Sorry for this lengthy post but I got carried away:

I wouldn't necessarily exclude Copyable from having a declaration or being used as an existential (which is what I think you mean by Copyableoids). While the exact details about what Copyable really is is left to a forthcoming proposal (along with generics and noncopyable vlues), Copyable could in some respects be a typealias of Any. SE-390 only describes what ~Copyable means for a type in its inheritance clause.

I'd say AnyObject, Sendable, and Copyable are three loosely related concepts in the language, but not fully protocols. What they all share in common with protocols is their ability to constrain a type variable. My interpretation of a protocol, in its purest form, is something that describes the types of a subset of its conformer's non-private members. In this sense, a protocol doesn't tell you about the conformer's in-memory representation (structs and classes can conform) or what private members it has (implementation detail). Conceptually, protocol requiring a private member to exist doesn't make sense, as that member is not going to be accessible via the protocol. An empty protocol doesn't tell you anything about its conforming types; it just creates a new type functionally equivalent to Any.

Ways AnyObject isn't quite a protocol: it allows you to assume its values have a uniform representation in memory as a class object. For example, when you dynamically cast a struct value to AnyObject, it gets boxed up into an implementation-defined class (called SwiftValue). There's no reason to ever explicitly state conformance to AnyObject, nor emit "conformances" to it, since it's an aspect of a type that can always be determined solely by looking at the type's interface in a module (in fact, its members are irrelevant, just "is this a class/actor?"). Thus, it's not necessary (and in fact, disallowed) for programmers to explicitly state AnyObject conformances. Only protocols can inherit from AnyObject, to make them "class-only" protocols.

Implementation Note

Within the compiler, AnyObject has no declaration and is "built-in" as a specially-marked empty protocol composition (like Any). Of course, there's no reason it has to be that way. It totally could be re-implemented as a protocol declaration in the stdlib, with some special treatments. But if we were to reimplement it, I think it'd be better phrased as some new concept, like a "layout constraint", that remains a built-in notion.

Sendable allows you to assume that it's safe to share the value between tasks, but the reasons why it's safe varies depending on the kind of conformer. For example, a class can't be Sendable if it has a mutable private stored property. So Sendable is not quite a protocol either. But unlike AnyObject, a record of whether a specific type can be considered Sendable is needed because you cannot determine based on a type's module interface alone. I believe this is what led to marker protocols: no stated requirements, thus no runtime metadata, but types must state whether they conformed.

We're still building-out what Copyable is, but here are my thoughts: For both programmer convenience and backwards compatibility reasons, we have to be able to determine whether a type is Copyable without having any explicit "conformances" written in a type's interface. So this has flavors of AnyObject. On the other hand, a type must be able to explicitly opt-out via ~Copyable, and that opt-out has to simply be recorded in the module. So the opt-out has flavors of Sendable. In addition, we'd eventually want ~ to work on more things than Copyable, so that probably blurs the line even further.

But sometimes blurring the lines is OK. I don't think ordinary programmers need to worry about precisely what these things are. They only need to understand how to use them, and "protocol" terminology is a fine starting point.

1 Like