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

I wrote that bit of the Generics Manifesto many years ago, and that last part has held constant for many years since then: the vast majority of requests we get for "parameterized protocols" or "generic protocols" specifically ask to be able to write Collection<String> to get a collection of strings, which aligns with this pitch. Let's call that interpretation #1.

There are other interpretations we could give to this syntax as well, so let's consider them.

Interpretation #2 is multi-Self protocols, as Joe points out. The canonical example is expressing conversions, e.g., ConvertibleTo<Double, Int>:

protocol ConvertibleTo<Other> {
  func asOther() -> Other   // "Self" is the type we convert from, "Other" is the type we're converting to
}

Except in Swift we use converting initializers, so maybe that should have been ConvertibleFrom<Int, Double>?

protocol ConvertibleFrom<Other> {
  init(_: Other) 
}

We have to decide what type to privilege as Self, because all of the members of the protocol are on that Self. What if you want both the initializer (on the "to" type) and the asOther operation (on the "from" type), how would you express that? Joe's straw man syntax

protocol Convertible(from: T, to: U) {
  // ... 
}

would make it clear that both the "from" nor the "to" type are on equal footing, with neither being functionally dependent on the other, which is the right semantics for this protocol.

Interpretation #3 is what Michael mentions in this post, where one wants to conform the same type to the same protocol in multiple different ways. There is still a primary Self type (so it's not the multi-Self case from interpretation #2), but it can bind the generic parameter in various different ways. For example, this might mean that String conforms to Collection (of Character) and Collect (of UnicodeScalar) and maybe others. Note that we could allow this with the same syntax we have today by lifting the restriction on overlapping conformances and allowing one to somehow specify that a particular member is only visible through the protocol conformance, e.g.,

extension String: Collection {
  typealias Collection.Element = Character // only visible through this conformance to Collection
  // ...
}

extension String: Collection {
  typealias Collection.Element = UnicodeScalar // only visible through this conformance to Collection
  // ...
}

There are lots of details here with ambiguity resolution and such, because when you have a primary Self type it still makes sense to say "String is a Collection " but that statement becomes ambiguous. Call sites will be able to sort through the ambiguity if you have other information, e.g.,

func find<C: Collection>(_ value: C.Element, in collection: C) -> C.Index? { ... }

let char: Character = a
find(char, in: "Hello, world")  // okay, only the String conformance where Element == Character matches

Indeed, you start to want to have a convenient way to say "String is a Collection where the element type is Character". The natural syntax for that is probably Collection<Character>, which is interpretation #1 that's being pitched.

Perhaps you have another interpretation of "generic protocols" in mind, or disagree with my analysis of interpretations #2 and #3, but I think both #2 and #3 are better expressed with a syntax that isn't of the form "protocol A<B>", whereas #1 (this pitch) is a highly-requested shorthand.

Doug

11 Likes