[Pitch 2] Light-weight same-type requirement syntax

Holly has covered this to a significant degree, but let me try to lay out the grand vision for where this is going.

We have an opportunity for a synthesis across several different language features:

  • We'd like generics to have stronger language connections to other things so that picking up generic programming feels more familiar, with better progressive disclosure of complexity.
  • We'd like to be able to express more advanced constraints on existential types (protocol and protocol composition types) than what you can do with just &.
  • We'd like to be able to express more advanced constraints on opaque result types than what you can do with just &.

The synthesis is quite simple. The existential and opaque result type cases require us to add a syntax to constrain the associated types of a protocol or protocol composition. This should, of course, be the same syntax for these two cases, other than the leading any or some keyword. Meanwhile, SE-0341 has introduced opaque parameter types, also written with some P, allowing the generic signature to be completely elided for some simple generic functions. By adopting the same syntax for the opaque-parameter some P as for the opaque-result some P, we can now write fairly advanced generic functions without an explicit generic signature. We only need a signature when there has to be a relationship between different components of the function (like if two collections need to have the same generic element type).

It's fair to ask: why does eliding the generic signature help to achieve the goal of building stronger connections to other parts of the language? Well, Swift has three ways to generalize over different types of values. One of them is subclassing, and that's inherently a limited form of generalization: it only works when you've got classes with a common superclass. The other two are generics and existential types. SE-0341 lets you express simple generics with almost the same syntax as existential types, just varying between any and some. The vision here is to generalize that to any sort of self-contained constraint, so you can you can take that and use it uniformly throughout the language, either as any or some. That is a very strong connection. And building that connection to existential types also makes generics much more familiar to programmers coming to Swift from one of our many peer languages with weak (if any) generic systems, where they're used to working with protocol types because that's the primary tool for generalization.

So that's the vision. It's an ambitious vision, in two main ways.

First, applying this same syntax to each of these features poses different implementation complexity.

  • For generics, this is pure "sugar" — it can almost be handled in the parser — and so there is no special implementation complexity.
  • For opaque result types, I believe it's not quite so simple, but it's still fairly straightforward.
  • For existential types, there's quite a bit of plumbing and generalization that will be required in both the compiler and the runtime.

So the syntax will need to be implemented for each of these cases at different times, unless we're going to hold the whole thing up for a few releases until we've solved all the problems around generalizing existential types.

Second, generality often comes at a cost. Generic signatures are very general, but allowing for that generality makes generic declarations and constraints syntactically very different from everything else.

For example, the way that you constrain the element type of an opaque Collection type (C.Element == Int) is completely different from the way that you constrain the element type of a concrete collection type (Array<Int>). That generality means that code can also constrain the other associated types of Collection just as easily as they can Element, which is obviously necessary. However, it also creates a significant and unfamiliar gap that programmers have to overcome before they can write code that's generic over collections. So if the syntax we introduce for this looks like the contents of a where clause, we'll have achieved generality, but we'll also be missing a major opportunity to build a stronger connection to other things in the language and make generics feel more like a generalization of what programmers are already familiar with. (This is particularly true for collections, since many programmers coming from other languages are familiar with being able to write e.g. Collection<MyType>, and it seems very strange to them that Swift uses exactly this syntax but for some reason not for Collection.)

To make that concrete, it would be great for progressive complexity if you could simply take some existing function that works on a concrete generic type like Array and substitute Array out for some Collection:

func collect(widgets: Array<Item>) {
  for widget in widgets { ... }
}

func collect(widgets: some Collection<Item>) {
  for widget in widgets { ... }
}

So I think the concrete achievement of this vision has to include both:

  1. a fully general syntax that can express any constraint that a generic signature with a single type parameter could and
  2. a syntax that specifically lets you constrain associated types by equality with a given type.

This pitch only addresses (2). Procedurally, I think it's okay for a proposal to carve out a narrow case like that, in the interests of making incremental progress, as long as it doesn't prevent the more general case from being addressed. I don't think that's happening here. I don't know that developing the general syntax is hard in the way that it's been described a few times in this thread, but if both syntaxes are indeed necessary, it's fine to start with the narrower one that has greater immediate impact on the standard library.

I was more worried about this narrower syntax being inadequate in the short term even for common cases until @Joe_Groff helped me realize just how expressive nested some types could be. For example, some Collection<some Comparable> is a perfectly fine way of expressing what otherwise would have been <C: Collection> where C.Element: Comparable.

27 Likes