Move-only types and Any

I saw mentioning of move-only types here and there, and thinking about this topic, I realised there is an interesting challenge in introducing move-only types to Swift.

Currently Any is the top type in Swift, but also it is copyable. Making it move-only would be a source-breaking change. But keeping it copyable means that move-only types do not conform to Any, making it not a top type. Which is also confusing.

Just curious if this was already considered, and what is the plan here. Is there a third option that I missed?

8 Likes

This is an excellent observation! My guess is that the root of the type hierarchy would change.

Since every type in Swift is copyable, the current root of the type hierarchy (Any) is also assumed to be copyable. In order for non-copyable types to exist in this hierarchy, we'll need a new root of the hierarchy, which would contain all existing types (including the former root).

Nomenclature-wise, Any sounds like the perfect name for a root type, but since (at the time of its inception) there was no concept of a non-copyable type, it got "infected" by the assumption of copyability.

I imagine, in a future Swift release, the type system would look something like this:

@_marker
public protocol Existing { } // Compiler magic, all types implicitly conform to this protocol.

@_marker
public protocol Copyable { } // Compiler magic with implicit conformance behavior much like Sendable.

Before the breaking change:

@available(*, deprecated, renamed: "any Copyable")
public typealias Any = any Copyable

// New root type: `any Existing`.

After breaking change:

public typealias Any = any Existing

OR

// Don't re-introduce `Any‘ in favor of `any Existing`.
2 Likes

Any is already not a perfect top type; non-escaping function types cannot be converted to Any.

12 Likes

I'm noticing a tendency, where the Any type (which conceptually is the root of the type hierarchy) keeps losing it's assumed status of a true root type every time a new kind of type appears that doesn't fit the current meaning of Any. Even if the assumptions under Any keep expanding to incorporate the new type kinds, it will still be prone to being broken every time something new comes along.

In light of this, I'd argue that having the Any spelling is going to be more trouble than worth. Especially now that we have the any Protocol spelling and existentials were unlocked for all protocols, the list of reasons to use a catch-all Any has gotten shorter. Also, the current any Protocol qualifier has made the usage of potentially expensive existentials more explicit and verbose, so using an unqualified Any is working against that.

If instead we used a specific protocol for the current understanding of the root type, it would still be a sensible spelling for it if the root type changes. For example, any Copyable (which is semantically the current root type) would still sound reasonable when it stops being the root type, unlike Any, that would immediately become wrong and confusing.

I vote for removing Any forever and letting go of the concept of root type in favor of clearly indicating the required capabilities. At the end of the day, even if you have a "true" root type, you can't do anything with it if you can't type-cast it or otherwise do run-time introspection on it, which already turns it into an any Castable and/or any Reflectable.

Even with move-only types, I would expect copyability to remain part of the default baseline requirements on types. So when you write a generic type or function over <T>, that would also assume that T is required to be Copyable unless you say otherwise. Having Any align with <T> would still fit with that notion that copyability is part of the normal Swift language, IMO.

3 Likes

The same goes for inout. When ownership is introduced and borrowed inout variables become possible, they won't fit into Any either.

inout is different; it's an argument/borrowing modifier, not part of the type system. You can covariantly open an inout Any and pass it as a <T> inout T; this is what happens when you call a mutating method on an existential.

I agree that even with move-only types, copyability is still going to cover the vast majority of use cases. However, I'd like to underline a key distinction in semantics between existentials and generic constraints. When you have a protocol-bound existential, you're saying "conforming to this protocol is all this variable will ever do" (exploring other capabilities of the boxed value will necessarily put it into another box in another variable. Conversely, a generic constraint says "this variable can be capable of doing anything in the world, buy I only care about it conforming to this protocol.". With this distinction in mind, having generic constraints default to copyable, we'll artificially make a lot of code unjustly inapplicable to move-only types (especially when the constrained types are not directly manipulated). Conversely, in case of an existential, forgetting to make it copyable will actively forbid that value from being copied by anyone, unlike a generic constraint, where the limitation only applies to the declaration that it applies to.

I see. I stand corrected, thank you!

I'd like to talk a little about the first point around T being copyable by default and how that limits move only in generic code. I think the point is astute and is something that move only in Swift should address. One solution that I find compelling is to introduce a notion of a "Moveable" marker protocol. The intuition behind this protocol is that if one were to codegen a generic function that treated self as a move only value, then one /could/ pass copyable and move only values to it since copyable types are also moveable! So the idea is then to:

  1. Force any generic value that conforms to the protocol to be a move only value.
  2. Require that self is treated as a move only value in any methods that conform to protocols that inherit from Moveable.

This would then allow one to write interesting generic code that works for both copyable and move only values. The other interesting part is that this method leads to interesting back porting possibilities since we could change how a function is codegened (changing it to its move only form) with out breaking ABI compatibility and also allowing for functions to be passed future move only values.

5 Likes

That sounds very reasonable to me! I imagine that Copyable protocol would inherit from the Movable protocol and in generic contexts (unless we insert some default constraints) you wouldn't be able to do anything with that type unless you require it to be at least Movable.

In a world with Movable and Copyable how would we maintain the defaultly implicit Copyable marking of types, while somehow being able to make something move only? Do we just imply Copyable unless the type is Movable?

Do we even have a way to express that without compiler magic? (Though I suppose compiler magic is exactly what we are talking about in this thread.) You can’t define a <T> where T not P, right?

I guess so. Most likely <T> written in code will desugar to <T: Copyable> in AST. And when declaring protocols, if protocol does not inherit to Copyable or Movable explicitly, conformance to Copyable will be inferred automatically. Type cannot not conform to the root protocol, so I think this inference will work even if new root type will be cut out of Movable.

Also note that Copyable and Movable actually have requirements, but conformance is witnessed by VWT, not a regular protocol witness table.

If in the future escape analysis for arbitrary types is added, this would fix this, right?

func justPrintIt(_ x: @nonescaping Any) {
    print(x)
}

func withClosure(_ x: () -> Void) {
    justPrintIt(x) // Ok
}

In my opinion that will be unnecessarily complicated and confusing. I'd argue that generic types should never be implicitly constrained. The entire functionality of the generic parameter should be dictated by the needs of the implementation. In order for this system to be complete, we'll need a way to disable implicit constraining, which will end up complicating things even more.

Besides, adding this behavior is much easier than removing it. We could introduce the new protocols and the new root type and see how it goes. If people overwhelmingly complain about verbosity, we can solve that problem separately.

1 Like

Technically yes, but the restrictions on those types would be pretty severe; you wouldn’t just replace Any with @nonescaping Any everywhere.