There is a semi-private function _openExistential
, shipped no later than Swift 5.6, that makes this possible.
First, consider the following utilities:
protocol EquatablePair {
func perform() -> Bool
}
protocol MaybeEquatablePair {
func maybePerform() -> Bool?
}
struct Pair<T> {
var lhs: T
var rhs: T
}
extension Pair: MaybeEquatablePair {
func maybePerform() -> Bool? {
(self as? EquatablePair)?.perform()
}
}
extension Pair: EquatablePair where T: Equatable {
func perform() -> Bool {
lhs == rhs
}
}
Here, we have a conditional conformance of Pair
to EquatablePair
. This allows us to use self as? EquatablePair
to dynamically determine if T
is Equatable
. The MaybeEquatablePair
conformance uses this trick to produce a boolean result if T
is Equatable
and nil
otherwise.
The next part is to get Pair<T>
for some concrete type T
. We really need to get Pair<T>
and not Pair<Any>
. This is what helps me distinguish this subtle difference: Any
is nothing but a struct of two fields, one being the pointer to the wrapped value’s type and the other the pointer to the actual value. The reality is slightly more complicated but this should give you some intuition. In this way, 1 as Int
gives you a plain integer, where as (1 as Int) as Ayn
gives you a special struct, hence Pair<Int>
is very different from Pair<Any>
.
So how can we dynamically fetch some Any
’s wrapped value’s type and use that to get a desired Pair<T
>? Here comes _openExisential
:
func genericEqual(_ lhs: Any, _ rhs: Any) -> Bool {
func openLHS<LHS>(_ lhs: LHS) -> Bool {
if let rhs = rhs as? LHS {
return Pair(lhs: lhs, rhs: rhs).maybePerform() ?? false
} else {
return false
}
}
return _openExistential(lhs, do: openLHS)
}
_openExisential
takes a wrapped value and use it to call some generic function. This magic function will dynamically fetch the type for its first argument and use that to call the generic function dynamically. This is not possible using plain Swift, in which calls to generic functions must have types resolved statically.
_openExisential
can do more than Any
. You might have heard the term existential types. This function can, well, open existential containers. It is a very complicated topic. See the Swift Evolution proposal Implicitly Opened Exisentials if you are interested.
My code is simplified from Foundation’s implementation for AttributedString. They seem to have a set of utilities to call Equatable, Encodable, Decodable implementation for Any
s. Check out AttributedString.swift and AttributedStringAttribute.swift for more. Start with struct CheckEqualityIfEquatable
.