Pitch: Suppressed Default Conformances on Associated Types

Hi everyone,

Today, it is not possible to declare an associated type that does not require its
type witnesses to be Copyable or Escapable. For example, consider the Element
associated type of Queue below:

/// Queue has no reason to require Element to be Copyable.
protocol Queue<Element>: ~Copyable {
  associatedtype Element

  mutating func push(_: consuming Self.Element)
  mutating func pop() -> Self.Element
}

While the conforming type is itself permitted to be noncopyable, its Element
type witness has to be Copyable:

/// error: LinkedListQueue does not conform to Queue
struct LinkedListQueue<Element: ~Copyable>: ~Copyable, Queue {
  ...
}

This is an expressivity limitation in practice, and there is no workaround
possible today.

Here’s a proposal to fix this limitation! I’d be happy to hear people’s thoughts.

18 Likes

Can you discuss whether you've considered—and if so, the reasons for not adopting—a rule analogous to the conservative one we adopted in SE-0446 for conditional conformances?

Namely, instead of having users learn a new exception about implicit Copyable, we could require stating copyability or non-copyability explicitly.

In other words, for the example given above where the protocol has a ~Copyable associated type, we could say that it is always necessary to write either extension Queue where Self: Copyable, Self.Element: Copyable or extension Queue where Self: ~Copyable, Self.Element: Copyable.

Is that workable?

1 Like

I see no reason associated types shouldn't have this ability. +1 from me.

At first glance, I don't believe so, since any requirement to explicitly reference all associated types runs into the problem that the set of associated types may be infinite due to recursive conformance requirements (which is the same reason we can't make them all Copyable and Escapable without explicit disabling in the first place).

3 Likes

Would this require specifying ~Copyable for every one of the protocol’s associated types, including non-primary ones?

In this case, the rule I'm contemplating would operate in the other direction—that is, you'd have to specify whether Self is Copyable or ~Copyable if any associated type avails itself of the pitched feature. Ah, I think I'm misreading the pitched rule... hmm...

I believe that @xwu is suggesting any protocol extension of a ~Copyable protocol rhat also has a ~Copyable associated type ought to be required to state either one or “Self: Copyable” or “Self: ~Copyable”. That is, we require the copyability of just Self to be spelled out in this case.

Enumerating all type parameters is impossible though as Joe said, for example

protocol P: ~Copyable {
  associatedtype A: P, ~Copyable
}

An extension of P now has an infinite sequence of distinct type parameters Self.A, Self.A.A, etc all of which are noncopyable. There’s no way to make them all Copyable.

1 Like

Is there a useful middle-ground here, where we force (or at least, warn) people to restate the fact that Self.A : ~Copyable in extensions of the protocol P ? This would apply only to the top-level associated types declared within the protocol itself, and not those recursively enumerable from it like Self.A.A or those inherited by the protocol. Perhaps this is sufficient for @xwu ‘s original concern?

This restatement would serve primarily as a call-out, to avoid confusion with the fact that Self: Copyable actually is implicit for any unconstrained protocol’s extension, but its suppressed associated types have no such magic:

protocol Queue: ~Copyable {
  associatedtype Element: ~Copyable
  ...
}

// (1). `Self: Copyable` is implicit
extension Queue {}

// Same as (1). More explicit.
extension Queue where Self.Element: ~Copyable {}


// (2) Fully unconstrained
extension Queue where Self: ~Copyable {} 

// Same as (2). More explicit.
extension Queue where Self: ~Copyable, Self.Element: ~Copyable {} 

Personally I think the trade-off is that for expert developers with a deep understanding of the type system, or those who work with protocols declaring many associated types, the call-outs may become low-signal boilerplate. But for readers writing “ordinary” code, or those with only the understanding of “Copyable/Escapable by default” in their minds, it may be useful as a stylistic warning for them to see it restated.

It seems this would be source breaking if the protocol adds a new associated type.

1 Like

I had some discussions about whether we want associated types to have a defaulting rule similar to what we devised for generic type parameter and extension declarations, where even if a protocol does not require Self: Copyable or Equatable, extensions and type parameters will require those protocols unless they explicitly suppress those requirements:

protocol Foo: ~Copyable { }

func foo<T: Foo>(...) // T still requires `Copyable` by default

func bar<U: Foo & ~Copyable>(...) // U is not required to be `Copyable`, explicitly

As Kavon's pitch notes, we can't extend this defaulting behavior to all associated types, at least with the type system features we have today, because a protocol with recursive requirements can have infinite associated types, and if all of those associated types were Copyable by default, it would be impossible to suppress all of those defaults. Nonetheless, the reasons for the defaulting rule for generic parameters seem potentially interesting for associated types as well:

  • The defaults maintain "progressive disclosure", where code that does not explicitly traffic in non-Copyable or Escapable types is not required to grapple with ownership or lifetimes.
  • The defaults allow for source stability when retrofitting generalizations onto existing libraries, particularly the standard library, so that types like Optional and protocols like Equatable can (or will be able to) generalize to work with non-Copyable and non-Escapable types without affecting existing code.

Although we can't provide a blanket defaulting mechanism for all associated types, one idea that we've revisited from the original SE-0390 discussions is the idea of allowing the protocol declaration to control what associated types, if any, have defaulting behavior. I added some discussion to the proposal with a few shapes of how this might look:

However, in discussion with the standard library team and a few other developers, we haven't identified a place among ourselves where we would definitively need such a feature. The standard library protocols that are good candidates for generalization, such as Equatable, Hashable, and Comparable, don't have any associated types, and the Sequence/Collection series of protocols is likely to benefit from deeper changes to make efficient use of ownership and lifetimes, so there are likely to be new protocols to handle their generalization anyway. But I wanted to hear from the community here as well: would you make use of an associated type defaulting feature if it were available?

1 Like