You can often get away with casting one value to the type of another (à la @bbrk24's suggestion) but there are occasionally situations where you need to validate multiple constraints, and simple casting can't get you there (since you can't insert where
clauses into a cast, and we don't have the if where
swiss knife yet).
There's actually a really neat workaround in situations like these, based on conditional conformance + the "witness" pattern. I first saw this used in The Composable Architecture for a type-erased Equatable test, but it can be extended a lot further. The general idea is to create a dummy generic type that conditionally implements a method inside a where
constrained extension. For example:
private protocol EquatableWitness {
var areEqual: Bool { get }
}
private struct Witness<L, R> {
let lhs: L
let rhs: R
}
extension Witness: EquatableWitness where L: Equatable, L == R {
var areEqual: Bool {
// we're effectively inside `if where L: Equatable, L == R` here
lhs == rhs
}
}
func areValuesEqual<L, R>(lhs: L, rhs: R) -> Bool {
let witness = Witness<L, R>(lhs: lhs, rhs: rhs)
if let equatableWitness = witness as? any EquatableWitness {
// we've proven to the compiler that the constraints
// are met, with just one test
return equatableWitness.areEqual
} else {
// the constraints weren't met
return false
}
}
Another hypothetical example — this one uses parameterized existentials but there are slightly less efficient ways to achieve the same thing without them. One can observe that this pattern is especially useful for type erasure.
private protocol CollectionWitness<C, E> {
associatedtype C
associatedtype E
func replaceFirstElement(of collection: inout C, with value: E)
}
private struct Witness<C, E> {}
extension Witness: CollectionWitness where C: MutableCollection, C.Element == E {
func replaceFirstElement(of collection: inout C, with value: E) {
// if where C: MutableCollection, C.Element == E
collection[collection.indices.first!] = value
}
}
func replaceFirstElement<C, E>(of collection: inout C, with value: E) {
if let witness = Witness<C, E>() as? any CollectionWitness<C, E> {
witness.replaceFirstElement(of: &collection, with: value)
}
}
In the general case, say you have generic parameters A, B
. You can implement if where COND { BODY }
over A, B
as
private protocol WitnessProtocol<A, B> {
associatedtype A
associatedtype B
func body(a: A, b: B)
}
private struct Witness<A, B> {}
extension Witness: WitnessProtocol where COND {
func body(a: A, b: B) {
BODY
}
}
func method<A, B>(a: A, b: B) {
if let witness = Witness<A, B>() as? any WitnessProtocol<A, B> {
witness.body(a: a, b: b)
}
}