Comparing two `Any` values for equality: is this the simplest implementation?

I'm working through a Swift Talk video series that re-implements SwiftUI state management and at some point there was a need to compare two values of type Any for equality. The series was made in 2021 using Swift 5.4.

This is their implementation (based on more elaborate code by @anandabits).

// Check if the two values are Equatable and equal
func isEqual(_ lhs: Any, _ rhs: Any) -> Bool {
    func f<LHS>(lhs: LHS) -> Bool {
        if let typeInfo = Wrapped<LHS>.self as? AnyEquatable.Type {
            return typeInfo.isEqual(lhs: lhs, rhs: rhs)
        }
        return false
    }
    return _openExistential(lhs, do: f)
}

protocol AnyEquatable {
    static func isEqual(lhs: Any, rhs: Any) -> Bool
}

enum Wrapped<T> { }

extension Wrapped: AnyEquatable where T: Equatable {
    static func isEqual(lhs: Any, rhs: Any) -> Bool {
        guard let l = lhs as? T, let r = rhs as? T else {
            return false
        }
        return l == r
    }
}

I tried to modify the implementation to take advantage of features introduced in Swift 5.7, like unlocked existentials for all protocols and implicitly opened existential. I ended up with this implementation:

// Check if the two values are equatable and equal
func isEqual(_ lhs: Any, _ rhs: Any) -> Bool {
    guard let lhs = lhs as? any Equatable else { return false }
    func f<LHS: Equatable>(_ lhs: LHS) -> Bool {
        guard let rhs = rhs as? LHS else { return false }
        return lhs == rhs
    }
    return f(lhs)
}

Could this be made even simpler?

For context, this is the call site and suggestions on how to simplify could extend there as well:

    func equalToPrevious(_ node: Node) -> Bool {
        guard let previous = node.previousComponent as? Self else { return false }
        let m1 = Mirror(reflecting: self)
        let m2 = Mirror(reflecting: previous)
        for pair in zip(m1.children, m2.children) {
            guard pair.0.label == pair.1.label else { return false }
            let p1 = pair.0.value
            let p2 = pair.1.value
            if !isEqual(p1, p2) { return false }
        }
        return true
    }

Hardly. But your nested function has utility as an extension.

extension Equatable {
  func equals(_ any: some Any) -> Bool { self == any as? Self }
}

func isEqual(_ lhs: some Any, _ rhs: some Any) -> Bool {
  (lhs as? any Equatable)?.equals(rhs) ?? false
}

And I think it's better to throw errors when casting is impossible, rather than returning false, but that's not "simpler".

2 Likes

Looks much nicer, thank you! I haven’t come across some Any before, I’ll look into it.

Regarding throwing errors, I agree. This implementation is a bit too specific to the purpose of the library.

For what it's worth, f(x: some Any) ends up being shorthand for f<T>(x: T)

5 Likes

If you like one-liners then maybe:

func isEqual<A: Equatable>(_ a: A, _ b: Any) -> Bool {
	(b as? A).map { a == $0 } ?? false
}

func isEqual(_ a: Any, _ b: Any) -> Bool {
	(a as? any Equatable).map { isEqual($0, b) } ?? false
}

The two functions work in tandem. I was hoping that the compiler could select the first overload where at least a is Equatable but apparently it's not the case, it always calls the second one for some reason. But seems to work for any a and b.

1 Like

You don't have to use an extension, but I think it's better to have it encapsulated. If you want correct overload resolution, don't use Any. There's generally no reason to use Any instead of some Any.

func isEqual<LHS: Equatable>(_ lhs: LHS, _ rhs: some Any) -> Bool {
  lhs == rhs as? LHS
}

func isEqual(_ lhs: some Any, _ rhs: some Any) -> Bool {
  (lhs as? any Equatable).map { isEqual($0, rhs) } ?? false
}
2 Likes

Ah, that's even shorter, nice. I don't know why some Any is better (let alone sounds weird in English :laughing:) but it still doesn't help the compiler with selecting a shorter path.

A word of caution, optional values with incomparable base types that are nil are always equal. So an implementation that would take this into account would be more wordy.

And yes, extensions are always better, you don't want to pollute the global name space, but in this case I was just following the original.

That does not match the behavior of Equatable in the presence of subclasses. Specifically, == may return true when comparing an instance of a subclass against an instance of a superclass (or another subclass). But the generic approach returns false when the second type cannot be converted to the first.

For example, given the following setup:

func isEqual<LHS: Equatable>(_ lhs: LHS, _ rhs: some Any) -> Bool {
  lhs == rhs as? LHS
}

class A: Equatable {
  var x: Int = 0
  static func == (lhs: A, rhs: A) -> Bool {
    return lhs.x == rhs.x
  }
}

class B: A {}
class C: A {}

We get these results:

let a = A()
let b = B()
let c = C()

print(a == b, isEqual(a, b))  // true true
print(b == a, isEqual(b, a))  // true false
print(b == c, isEqual(b, c))  // true false
5 Likes

That's a great point.

The second test could be fixed by testing lhs as? RHS as well.

But I can't think of a solution for the siblings case. If there's a (generic) way to type cast an object to it's super class, then we can try going up the hierarchy and try to cast again until we exhaust all ancestor types.

How do people deal with that problem? Conform all of the classes they need to process to a protocol?

protocol EquatableSuperclass: Equatable & AnyObject {
  associatedtype Superclass: Equatable & AnyObject
}

extension A: EquatableSuperclass {
  typealias Superclass = A
}

extension EquatableSuperclass {
  func equals(_ any: some Any) -> Bool {
    if case let object as Superclass = any { self as! Superclass == object }
    else { false }
  }
}

func isEqual(_ lhs: some Any, _ rhs: some Any) -> Bool {
  switch lhs {
  case let lhs as any EquatableSuperclass: lhs.equals(rhs)
  case let lhs as any Equatable: lhs.equals(rhs)
  default: false
  }
}