Implementing AnyEquatable

⇐ 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 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:

In general, we think that state needs to consist of simple value types, and as soon as you introduce classes and objects you expose yourself to a lot of undefined behavior […]. If you need parts of these classes in your state we recommend defining structs that hold just the value-based [representations].

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.

Types conforming to any style protocol, such as ButtonStyle, ToggleStyle, are now enforced to be value types. Styles must be structures or enumerations, not classes, and conforming a class to a style protocol may trigger an assertion. This is the same restriction that the system has always enforced on types conforming to View. (62886135)


  1. Another recent one was extending #136.2 for learning’s sake. 

  2. Which arguably covers a decent chunk of AnyEquatable usages, since Hashable inherits from Equatable