Implementing AnyEquatable
04 Mar 2021 ⇐ Notes archive(This is an entry in my technical notebook. There will likely be typos, mistakes, or wider logical leaps — the intent here is to “let others look over my shoulder while I figure things out.”)
Point-Free #133.1 is a hidden gem1. Outside the episode’s FormActionBindingAction context, it asks the viewer to implement a type eraser on the Equatable protocol. Here’s a quick note on the implementation because I’m still thinking about how wicked it is a month later — first, some scaffolding we’ll work under.
struct AnyEquatable: Equatable {
let value: // ???
init<Value: Equatable>(_ value: Value) {
self.value = // ???
}
static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
// ???
}
}
Presumably we’d want to expose AnyEquatable.value — but, we can’t make it of type Value, since that’d require threading the underlying generic at the type level, defeating the purpose of erasure in the first place. Maybe…Any? That’d further mean, ==’s implementation needs to remember value’s Value-ness if it has any hope of comparing it against rhs.value (since it’s another Any). So maybe we hold onto an Any-based predicate‽ It’s kinda wild, yet this seems to be the only implementation we can pull off sans compiler-generated magic like AnyHashable2.
struct AnyEquatable: Equatable {
let value: Any
private let valueIsEqualTo: (Any) -> Bool
init<Value: Equatable>(_ value: Value) {
self.value = value
valueIsEqualTo = { other in other as? Value == value }
}
static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
lhs.valueIsEqualTo(rhs.value)
}
}
(!).
Giving it a spin lets us mix and match erased values of the same or different underlying types.
AnyEquatable(5) == .init(4) // ⇒ false
AnyEquatable(5) == .init(5) // ⇒ true
AnyEquatable(5) == .init("Five") // ⇒ false
Er wait, hmm — I looked into why this isn’t already built into the language (à la AnyHashable) and it turns out this implementation breaks with classes and subclassing. Maybe TCA gets away with this since it’s recommended that the State graph be composed entirely of value types:
The library also fences off misusing AnyEquatable by embedding value and valueIsEqualTo into BindingAction, directly.
And as a final tangent, SwiftUI prevents the similar problem of conforming reference types to View, ButtonStyle, et al by trigging runtime assertions.
-
Another recent one was extending #136.2 for learning’s sake. ↩
-
Which arguably covers a decent chunk of
AnyEquatableusages, sinceHashableinherits fromEquatable. ↩