Swift's notation for generic constraints generally requires naming the things being constrained; you say that a particular generic parameter conforms to a protocol by naming the generic parameter and the protocol it conforms to, like <C: Collection>
, and you put further constraints on its associated types by naming them in a where clause, like C.Element: Equatable
. However, opaque result types, generalized existentials, and other conceivable future language features need a way to describe constraints on a type that doesn't otherwise have a name; an opaque type is intentionally hidden from the interface, and an existential's contained type is dynamic and can change at runtime. I can see this evolving into its own major design discussion so I figured it's a good idea to spin this off from the initial opaque result types proposal.
To kick things off, I'd like to suggest borrowing another idea from Rust here: In Rust, you can use T: Trait<Assoc = Type>
as shorthand for a combined constraint that T
implements Trait
and that T.Assoc
is same-type-constrained to Type
, as if you'd written T: Trait where T.Assoc = Type
. This shorthand can also be used in Rust's equivalents of opaque types (impl Trait<Assoc = Type>
) and existentials (dyn Trait<Assoc = Type>
) in addition to generic type constraints. If we were going to do something similar for Swift, we could generalize it a bit so that the shorthand can be used for both protocol and same-type constraints on associated types and to allow constraints between associated types. The result might look something like this:
// Leading dot is used to refer to an associated type
T: Protocol<.Type == Int> // T: Protocol where T.Type == Int
T: Protocol<.TypeA == .TypeB> // T: Protocol where T.TypeA == T.TypeB
T: Protocol<.Type: OtherProtocol> // T: Protocol where T.Type: OtherProtocol
We can allow this syntax in any context where we allow generic constraints, including as part of existential, composition, or opaque result types. The shorthand can express any set of constraints we have today without having to name the constrained type. In addition to providing a usable syntax for these new language features, I think it's also a nice, more compact encoding for generic constraints in general. Being a variation on Rust's design, it aligns with another prominent language, and it also draws syntactic analogy to other languages that use generic interfaces instead of associated types to describe generic constraints among multiple types.
The main alternative to this design I see is one that's come up in previous iterations of discussions about existentials and opaque result types. Many people have suggested to use a standard where clause with a placeholder like _
to name the unnamable type. For an opaque result type, this would look like:
func foo() -> some Collection where _.Element == Int { ... }
and for a generalized existential, it might look like:
var myInts: Any<Collection where _.Element == Int>
This has the advantage of being an incremental extension of Swift's existing syntax, and it definitely reads better as a sentence. However, I'm not a fan of this direction for a number of reasons:
-
This overloads the
_
token, something Swift has thus far managed to avoid for the most part (and a common complaint about Scala in particular). -
For opaque result types, there's only one
where
clause for the entire declaration, and that where clause would commingle constraints on the opaque return type itself with constraints on the function's other generic arguments, meaning the opaque type is no longer syntactically self-contained. This is an implementation and readability challenge. -
The magic token
_
only scales to one implicitly-named opaque or existential thing. In the fullness of time, one could imagine a function supporting multiple opaque return types:func twoCollections() -> (some Collection, some Collection)
or Swift also growing to support
some
notation on arguments:func duplicate(_ collection: some inout RangeReplaceableCollection) -> some RangeReplaceableCollection
Existentials too could conceivably generalize to the point that there are multiple existentially-qualified generic types at play. Using
_
doesn't answer the question of how to apply constraints individually to each anonymized types in these situations.
The Protocol<.AssocType == T>
shorthand, by contrast, doesn't rely on a magic token, so it avoids overloading an existing token like _
or ascribing magic meaning to a magic identifier. Furthermore, it generalizes well to multiple anonymous things, since the set of constraints on each opaque thing can be written in a self-contained notation. Here's a table to compare the shorthand I'm proposing with the where
clause placeholder:
Feature |
Protocol<...> notation |
where notation |
---|---|---|
Generic constraint | <T, U: Protocol<.A == T, .B: P>> |
<T, U: Protocol> ... where U.A == T, U.B: P |
Opaque type | func foo<T>() -> some Protocol<.A == T, .B: P> |
func foo<T>() -> some Protocol where _.A == T, _.B: P |
Existential | var x: Protocol<.A == Int, .B: P> |
var x: Any<Protocol where _.A == Int, _.B: P> |
I'd be interested to hear other suggestions as well as feedback on these two possible approaches. Thanks!