Motivation
Currently, Swift generics support only nominal constraints (T: U, T == U).
However, there are many practical cases where a type is castable but not subclassing or equal.
The compiler is already capable of determining these relationships at compile time, since the as operator performs a compile-time checked cast.
Still, there’s no way to express such “castability” in generic constraints.
A clear example is the Combine framework.
The subscribe method requires strict equality between a publisher’s Output and a subscriber’s Input, and also between their Failure types.
Yet in practice, these types are often safely castable, and the compiler already knows it:
// This could be valid in theory:
// - Output `Int` can be lifted to `Int?`
// - any `Failure` can be treated as `Error`
// - `Never` can be treated as any type.
AnyPublisher<Int, SomeError>.mock
.subscribe(AnySubscriber<Int?, Error>.mock)
Currently, developers must add unnecessary bridging steps:
publisher
.map { $0 as Int? }
.mapError { $0 as Error }
.subscribe(subscriber)
It’s possible to write additional subscribe methods for such cases, but that approach is verbose and leads to ambiguity.
Just to cover the examples above, one might need around 20 overloads (4 main cases plus ~16 ambiguous combinations).
These conversions are trivial, type-safe, and already known to the compiler, yet require explicit modifiers and additional overloads.
Also, this issue may become more relevant with the introduction of typed throws in Swift 6.
Proposed solution
Introduce a new form of castable generic constraint, for example:
func subscribe<T, F: Error>(_ subscriber: some Subscriber<T, F>)
where T is Input, F is Failure {
// ...
receiveValue: { (input: T) in
subscriber.receive(input as Input)
} receiveError: { (error: F) in
subscriber.receive(error as Failure)
}
// ...
}
This would mean:
“The compiler should accept this generic function if
Tcan be safely cast toInputusingas.”