Generalized supertype constraints

This topic is not new, it was previously pitched by @anandabits here and there is also a never merged documentation PR for this idea:

As ABI is finally stable it was a fresh breeze to see new generics related pitches arise in SE. I would love if we as the community could push together this topic (if it simplifies things I would start only the supertype constraint and separate the generic associated type (see the PR) out into it's own pitch/proposal).

To kick off the discussion I want you to consider this simple case where it's clearly known by the reader that the first associated type must be a class.

class Storage {}
protocol P {
  // A is known to be a class as it's a sub-type of `Storage`
  associatedtype A: Storage
  associatedtype B: A // error: Inheritance from non-protocol, non-class type
}

The error message raised by the compiler seems like an unfortunate historical restriction.

We can take this example and generalize any supertype constraint in the language even further, to allow a more flexible way of expressing type relation constraints.

The example only shows the generalization in context of a protocol, but it could be applied everywhere where we can express type relation constraints.

protocol Q {
  associatedtype S
  // - If `S` is a value type then `T: S` would be viewed as `T == S`
  // - If `S` is a class type then `T` is either `S` or a sub-class of `S`
  // - If `S` is a protocol/existential then `T` must conform to `S`
  // - If `S` is a function type then `T` must be a function type that is subtype of `S` 
  // - If `S` is a metatype `U.Type` then `T` must be a metatype `V.Type` where `V.Type: U.Type`
  associatedtype T: S // error: Inheritance from non-protocol, non-class type 'Self.S'
}
7 Likes

The above quote is unrelated but applicable to this discussion.

@DevAndArtist thanks for reviving this topic. I would love to see it pushed forward. Please keep in mind that the idea behind this is to support truly generalized supertype constraints that recognize all of Swift’s subtyping rules, including rules such as T: T? and (() -> Void): (() -> throws Void). So in your last example, it would be valid to have S == Int? and `T == Int.

@masters3d regarding specific problems, I have not done a good job of keeping track of the countless use cases I have run into for this feature. That said, there are many of them.

One example is generic code where there is an associated type or generic parameter that must alway inherit from UIView, however in some cases a context will want to specify a more refined base view class. You end up wanting to specify a constraint that a parameter is a subtype of the base view but can’t because the base view itself is an associated type or a generic parameter.

Another use case is code that mediates between the provider of a value and the receiver of a value. Sometimes you would like the mediating code to be able to accept values of any type that is compatible with the receiver, not only the exactly type specified by the receiver. This makes the mediator compatible with more providers and reduces the need to write boilerplate that sometimes just amounts to { $0 } where a type conversion is performed.

I don’t have time to reconstruct a full code examples right now but I hope these descriptions at least point in the general direction of the problems this feature solves.

4 Likes

// - If `S` is a protocol/existential then `T` must conform to `S`

This should also allow for T to be the S existential once again.

Protocols do not conform to themselves, so this is not possible.

Not in general, but Error now does and others may in the future. For example, there has been some discussion of an eventual ability to explicitly declare conformances for existentials. Also, AnyHashable is an odd beast - not technically an existential, but playing the role of one - and it conforms to Equatable and Hashable.

The present pitch is phrased generalized supertype constraints specifically to be clear that the feature is intended to work with all valid subtype relationships in Swift, both when it ships as well as those added in the future. If the concrete types in a conformance or call to a generic function fail to meet the specified supertype constraint a compiler error is produced indicating why that constraint was not met.

1 Like

Right, exactly. But to be clear, it will not itself make currently invalid relationships, valid. That is, in the general case, protocols will not conform to themselves here or elsewhere because of this feature.

1 Like

For sure. This pitch does not introduce any new subtype relationships, it only introduces a new way to use the subtype relationships that are available.

1 Like

That is totally fine and I fully agree here. I also think that every contributor to this thread should understand that the general idea is great and so but it's nothing trivial we would like to tackle here. There are clearly multiple issues that requires discussion and design with people knowledgable in type theory. I can foresee that the validation of generalized supertype constraints should come in multiple stages as it's quite easy to tap into areas where from the readers perspective the constraint might make 'some' sense, but the type theorist and the implementor could identify issues like some 'infinite metadata look up recursion' (please correct me if I said wrong terminology here).

The first milestone of this discussion would be to agree on a definition of the generalized supertype constraints that will go into the generics manifesto and is approved by the core team as a future evolution direction of the language.

When I asked @Douglas_Gregor before opening the generics manifesto PR it sounded like this was not a “large” request (relative to other generics features).

Well since I'm not a compiler developer nor a type theorist I can't tell for sure how huge or small the impact on the implementation will be when lifting these restrictions. My assumptions come accumulated from other threads where things such as Self == Generic<Self.T> came up that are not necessary easy to spot right away. So forgive me if I might said something that is not correct. If the general idea is simpler then I view it, then it's even better. :slight_smile:

1 Like

One example that I just wanted to express is generic upcast method.

protocol P {}
extension P {
  func upcast<T>(to _: T.Type = T.self) -> T where Self: T {
    return self as T
  }
}

If this would be possible I would fix this statically unsafe method and remove the precondition:

struct DriverFor<Base: AnyObject> {
  let base: Base
  init(base: Base) {
    self.base = base
  }
}

protocol Drivable: AnyObject {}
extension Drivable {
  // FIXME: Remove 'precondition' and add `where Self: Base` constraint
  func driver<Base>(for _: Base.Type = Base.self) -> DriverFor<Base> {
    precondition(self is Base)
    return DriverFor<Base>(base: self as! Base)
  }
}
2 Likes

I ran into a similar problem for which this would be a great solution, so allow me to revive this thread.

It seems the feedback so far has been mostly positive, and I agree with the (quite well-written) wording on @anandabits' pull request.

What would be the next step? Does the pull request need to be updated (due to possible changes in master since then)? If not, should we maybe ping someone from the core team to help get this merged?

I personally would tend to say that the PR should remove generic associated types as it's a separate topic we need to discuss on SE before adding it to the manifesto. I'm not saying that I don't like it, it's the opposite, but I would like to be more explicit on what we want most and what we want to be able to push first. @anandabits thoughts?

I don’t see why we would remove generic associated types unless specifically asked by the core team. It’s a natural extension of the existing generics system.

This PR targets the manifesto so there is no specific timeline or commitment to a specific design. It only adds comments about this feature to a document people can refer to in the future. I think it would be good think if the Swift project maintained living roadmap documents like this for major feature areas. We should update them when new ideas that naturally extend the existing model in obvious directions are identified.

3 Likes

Trying to reuse code and making more generic things, it's getting annoyed to use type erasure all the time. It turn into common workaround in the community because Apple uses it in the standard library but still it's a workaround.