I want to use parameter packs to provide a type-safe query API. But I can’t figure out how my query type’s generic subscript can enforce that its type parameter is in the parameter pack that was passed to the type’s type parameter. Here’s my declaration so far:
struct Query<ElementTypes: each Element> {
public subscript<E>(_: E.Type = E.self) -> E { }
}
I want to tell the compiler that E must be an element of ElementTypes. Alternatively, I could make the subscript non-generic if I could tell the compiler that for each element X of ElementTypes, there exists a public subscript(dummy: X.Type = X.self) -> X.
Is either of these possible express in Swift today?
The closest I know of that is possible, does not meet your requirements:
@inlinable public func first<each Element, Match>(
type: Match.Type = Match.self,
_ element: (repeat each Element)
) -> Match? {
for case let match as Match in repeat each element {
return match
}
return nil
}
Of course, using parameter packs in functions generally crashes the compiler, so you can't actually usefirst within Query. You have to copy and paste the code instead.
struct Query<each Element> {
let elements: (repeat each Element)
subscript<Match>(_ type: Match.Type = Match.self) -> Match? {
for case let match as Match in repeat each elements {
return match
}
return nil
}
}
let query = Query(elements: ("one", 2))
#expect(query[] == 2)
A good way to think about "new" kinds of requirements is to consider what happens if you observe it from a different generic context. For example, if you have this, it's not so bad, you'd just have to re-state your "is an element of pack" requirement relating E with each T on f():
func f<each T, E>(..., e: E) {
let q: Query<repeat each T>(...)
let value: E = q[]
}
However, now let's suppose we use your Query type like this:
func f<each T, each U, E>(..., e: E) {
let q: Query<repeat each T, repeat each U>(...)
let value: E = q[]
}
This time, you'd have to statically declare that E is an element of each T, oreach U, so suddenly you need to introduce "disjunction requirements" in your generics system to be able to use this in full generality. Or what about this:
func f<each T, E>(..., e: E) {
let q: Query<repeat each T, Int>(...)
let value: E = q[]
}
Now the subscript is valid only if E == Int (same-type requirement), orE is an element of each T, so now you've got to be able to deal with disjunction requirements that contain arbitrary other requirements, not just your new kind.
So I suspect the original idea of a "is an element of pack" requirement is probably unworkable, because it doesn't generalize very well.
I believe the E in Elements version implies the ability to treat any type sequence (aka generic parameter pack) as a disjunction. It doesn’t just arise in certain special cases.
What about the alternative of declaring that for each element X of Elements, there exists a subscript specialized for that X? I don’t think that would risk introducing problems, since I could write the same thing out by hand. And it would achieve my original goal of type-safe access to the fields in a query result.
This would require a new form of dynamic dispatch if you used your subscript from a generic context. And if you’re not using your subscript from a generic context, such a declaration wouldn’t really do anything.
I do think there is a benefit in a non-generic context. My broader goal is to create a Transaction protocol that specifies what Elements it reads from and writes to. This would enable my program to automatically determine transaction dependencies and parallelize those without conflicting dependencies. The reason for doing this in the type system is so that the Transaction conforming type cannot accidentally read from or write to Elements it has not statically declared.