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 FormAction
BindingAction
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 AnyHashable
2.
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
AnyEquatable
usages, sinceHashable
inherits fromEquatable
. ↩