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

The "forcing" and the "sneaks" and the "takes away" come across as frantic and antagonistic. They are neither the tone we would like to set on these forums, nor are they helping your argument.

Fundamentally, your complaint is that this proposal is not syntactic sugar because, for opaque result types, it expresses something new: the ability to provide constraints on the associated types of the opaque result type.

As far as I can tell, you aren't disagreeing with that new feature, i.e., you agree that it is useful to be able to express a result type that is "some Sequence where the Element type is String". Your disagreement seems to come in two parts:

  1. You don't like taking the syntax Sequence<String> for this purpose, because it prevents us from using that syntax for something else in the future.
  2. You want the ability to express a complete where clause for the opaque type, rather than this restricted form.

Regarding (1), I don't actually think we want to use this syntax for the other things that "generic protocols" could mean. Here's my attempt at enumerating those things and why they should be spelled differently. It's not enough to say that generic protocols might need this syntax later; you actually need to make a strong case that this specific syntax is the best syntax for that future feature.

Regarding (2), the full "reverse generics" feature has an explicit type parameter list on the right-hand side of the ->. For example, let's write an "unzip" of sorts:

func unzip<C: Collection, T, U>(_ collection: C) -> <R1: Collection, R2: Collection> (R1, R2)
  where C.Element == (T, U), R1.Element == T, R2.Element == U { ... }

In other words, pass in a collection whose element type is (T, U) and get two collections back, one with the T's and one with the U's. With this proposal (and SE-0341), this can be expressed as:

func unzip<T, U>(_ collection: Collection<(T, U)>) -> (some Collection<T>, some Collection<U>)

That is so much clearer. The reverse-generics formulation isn't just more cluttered, it's forcing you to actively reason about both generic parameter lists and detangle the where clause to understand which bits affect the generic parameters left of the -> and which affect generic parameters to the right of the ->.

Reverse generics are a good conceptual underpinning for opaque result types that precisely matches the implementation model. Indeed, they are implemented in the compiler behind an experimental flag so we could test out all of the complicated combinations and internally desugar this pitch to that implementation. However, it is not at all clear to me that we ever want to surface the full reverse-generics models to users: you have to go very deep into the theory for the reverse-generics desugaring of this pitch to make more sense than other more-accessible ways of understanding opaque result types. This pitch covers the most common cases in a manner that we can teach.

If we did eventually get some other way to do more arbitrary where clauses, e.g., this suggestion:

then that would likely cover the expressivity gap. But I would say that this typealias solution by itself is not good enough to replace this pitch. Would we create Of-suffixed typealias versions of all of the collection protocols in the standard library? C++ did this with their type traits, from original class templates (is_same), to value forms (is_same_v) and finally concept forms (same_as), and the result is an awful mess, bloating the API with 3 names for each idea. We should not knowingly go down the same path.

If supporting an arbitrary where clause is important to be able to express in the language, then that feature needs supporting examples. And if the argument is that the need for an arbitrary where clause is so great that we should block progress on this particular pitch... then it needs to demonstrate that this pitch is going in the wrong direction, rather than just that this pitch isn't going far enough.

Doug

9 Likes