# Dynamic equality checking, and Equatable

**URL:** <https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556>\
**Category:** Discussion\
**Tags:** equality, reflection\
**Created:** [May 15, 2019, 9:05pm UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556 "2019-05-15T21:05:50Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![NSExceptional](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/nsexceptional/32/6813_2.png) [@NSExceptional](https://forums.swift.org/u/NSExceptional)\
**Post date:** [May 15, 2019, 9:05pm UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/1 "2019-05-15T21:05:50Z")

</div>

## Background

Swift's `Equatable` is defined like this:

```swift
public protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

```

Note that the arguments for `==` are defined as `Self` rather than `Equatable`. This has some implications and benefits:

1. The arguments must be the same type at the call site.
2. Because of #1, the function is generic in a way; each type gets its own implementation instead of one shared implementation that would have to unbox arguments.
3. Because of #2, synthesized conformance to Equatable is fast. Boxing values carries overhead, and since the argument types are known at the call site, Equatable doesn't have to do any boxing.

However, there are some drawbacks:

1. The arguments _ **must be the same type.** _
2. By implication, the arguments must be _ **known** _ to be the same type.
3. The use of `Self` as in Equatable's definition restricts how `Equatable` can be used. You cannot declare collections or variables as a protocol type, like you can in Objective-C.

For example, #3 above makes any code similar to the following impossible:

```swift
let a: Equatable = ...
let b: Equatable = ...
a == b

```

As another example, by extrapolation, you cannot compare collections of Equatable either, unless they can be deduced to be the same type, such as within a generic function with the following signature:

```swift
func checkEqualArrays<T: Equatable>(a: [T], b: [T]) -> Bool

// This compiles
checkEqualArrays(a: [1, 2, 3], b: [1, 2, 5])
// This does not:
// "Protocol type 'Any' cannot conform to 'Equatable'
// because only concrete types can conform to protocols"
checkEqualArrays(a: [1, 2, 3], b: ["x", "y", "z"])

```

Going further down the rabbit hole, you cannot get around this by just adding an additional generic argument. This would allow the above code to compile, but the implementation of `checkEqualArrays` will not:

```swift
func checkEqualArrays<T: Equatable, U: Equatable>(a: [T], b: [U]) -> Bool {
    // Binary operator '==' cannot be applied
    // to operands of type '[T]' and '[U]'
    return a == b
}

```

## Manual type-erasure and AnyHashable

If you've ever been to this dark place, as I'm sure many of you have, like me, you found yourself frustrated and annoyed. Without any other options, you turned to some form of type-erasure. Type-erasure is a workaround of sorts, but it's absolutely something that should be avoided in these instances. `AnyHashable` is an example of this if you've ever used it: it allows you to compare two `Hashable`s for equality without ensuring they have the same concrete type at compile-time:

```swift
let a = AnyHashable(5)
let b = AnyHashable("x")
a == b // false

```

AnyHashable goes through a lot of boilerplate and uses what little reflection Swift currently offers to accomplish this. Unfortunately it doesn't alleviate the biggest problem I have with Equatable (and Hashable, by extension), which is that you can't declare variables or collections to store them directly, like you can in Objective-C.

## Example: JSON objects and `Any`

Many APIs vend `Any`, such as `JSONSerialization`'s `.jsonObject(_:_:)` methods. For testing purposes, you may want to compare the output of these methods to one another. You'll quickly find you can't:

```swift
let data: Data = ...
let json = (try JSONSerialization.jsonObject(with: data, options: [])) as! [String: Any]

XCTAssertEqual(json, otherJSON) // won't work

```

You might think you can get around this by making your own special protocol to replace the `Any` in that expression:

```swift
public protocol JSONValue: Equatable { }

extension Array : JSONValue where Element: JSONValue { }
extension Dictionary : JSONValue where Value: JSONValue { }
extension String : JSONValue { }
extension Date : JSONValue { }
extension Int : JSONValue { }
extension Bool : JSONValue { }
extension Double : JSONValue { }
...
extension NSNull : JSONValue { }

...
let json = (try JSONSerialization.jsonObject(with: data, options: [])) as! [String: JSONValue]

```

That last line sadly won't compile because of the `Self` requirements of Equatable. Type-erasure by hand could help here, maybe, but it's a very hacky solution to something the language itself should support.

## Informal Pitch: `OpaqueEquatable` (or something similar)

Enter `OpaqueEquatable`. It would be a new protocol whose operator would accept boxed parameters instead of `Self` restricted parameters (hence the 'Opaque'). At runtime, the parameters would be checked first for type equality, then actual equality. The implementation might resemble this pseudocode:

```swift
protocol OpaqueEquatable {
    static func == (lhs: OpaqueEquatable, rhs: OpaqueEquatable) -> Bool
}

extension OpaqueEquatable {
    static func == (lhs: OpaqueEquatable, rhs: OpaqueEquatable) -> Bool {
        if type(of: lhs) == type(of: rhs) {
            // Get method reference to what is currently
            // the synthesized Equatable implementation
            let isEqual: (?, ?) -> Bool = type(of: lhs).==
            return isEqual(lhs, rhs)
        }
        
        return false
    }
}

```

> Quick note: a definition like this isn't actually possible to use, because nothing can conform to it without giving this error: `Member operator '==' must have at least one argument of type 'T'` where `T` is the conforming type. I will ignore this for now though.

Ideally, everything—somehow including `Any`—would conform to this protocol, or at least be able to conform to it by opting-in within your own project:

```swift
extension Any: OpaqueEquatable { }

```

It might also make sense for one of OpaqueEquatable and Equatable to conform to the other, but I'm not sure which. I'll leave that up to discussion below, as I'm not sure what the benefits and consequences of `OpaqueEquatable: Equatable` vs `Equatable: OpaqueEquatable` are, or which one should be the default to make APIs as easy to use while still remaining performant.

With OpaqueEquatable, the following should be possible:

```swift
public protocol JSONValue: OpaqueEquatable { }

extension Array : JSONValue where Element: JSONValue { }
extension Dictionary : JSONValue where Value: JSONValue { }
extension String : JSONValue { }
etc ...

...
// OK
let json = (try JSONSerialization.jsonObject(with: data, options: [])) as! [String: JSONValue]
// OK
XCTAssertEqual(json, otherJSON)

```

Edit: as @anandabits mentioned, this doesn't necessarily have to be a protocol. Anything with the semantics of a top-level function like `isEqual(_ lhs: Any, _ rhs: Any) -> Bool` or better would be suitable.

### Edge cases

`Any` includes everything, even functions and closures. **As long as the behavior is documented,** I think any of these would be fine since it rarely makes sense to compare closures anyway:

- Throw a runtime error when comparing functions or closures
- Allow functions/methods to be compared, but not closures
- Allow functions/methods _and_ closures to be compared, where the same closure source-declaration is always equal to itself, regardless of captured variables

* * *

What are your thoughts and concerns? Would you find this useful or not?

---

<div class="post-metadata">

**Author:** ![anandabits](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/anandabits/32/355_2.png) [@anandabits](https://forums.swift.org/u/anandabits)\
**Post date:** [May 15, 2019, 9:19pm UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/2 "2019-05-15T21:19:35Z")

</div>

I agree that dynamic equality checking is something that would be useful to support.

We don't need to introduce a protocol though. It's possible to implement an API that takes two values of type `Any`, returning `true` when both values are of the same type and that type implements `Equatable` or `false` otherwise. The implementation requires some black magic but users don't need to worry about that.

---

<div class="post-metadata">

**Author:** ![NSExceptional](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/nsexceptional/32/6813_2.png) [@NSExceptional](https://forums.swift.org/u/NSExceptional)\
**Post date:** [May 15, 2019, 9:31pm UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/3 "2019-05-15T21:31:17Z")

</div>

Anything that simple is a win in my book. I thought a protocol might be more ergonomic though, since `Any` includes functions and closures, and we might somehow be able to exclude those from being used at compile time if we used a protocol with some compiler magic instead of relying on `Any`.

Then again, I can imagine scenarios where I might find it useful to compare functions. I've edited my post near the end to bring up this edge case.

---

<div class="post-metadata">

**Author:** ![Karl](https://avatars.discourse-cdn.com/v4/letter/k/34f0e0/32.png) [@Karl](https://forums.swift.org/u/Karl)\
**Post date:** [May 15, 2019, 11:07pm UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/4 "2019-05-15T23:07:43Z")

</div>

This topic has come up _many, many_ times before. [Here's one which shows some alternatives](https://forums.swift.org/t/adding-a-polymorphic-equatable/12034/10). I can't remember if one of the previous discussions includes an "official" answer for why Equatable is this way, but I assume one of them does. It's certainly a well-known limitation.

> [@NSExceptional](#):
>
> Type-erasure by hand could help here, maybe, but it's a very hacky solution to something the language itself should support.

What you propose (OpaqueEquatable) is also a kind of type erasure. I'm not sure what you mean by "by hand", but you can largely automate it with a protocol requirement + default implementation (see below).

> [@NSExceptional](#):
>
> For testing purposes, you may want to compare the output of these methods to one another.

If it's just for testing, you could test against a known deserialised copy or serialise it, then compare the data/string versions.

Anyway, here's a modified version of your example which includes a simple kind of "polymorphic" equatable. If it's just for testing, you can modify the `#if true` lines to something meaningful (I don't think we have a compile-time conditional for `@testable` builds, but that might be worth adding...):

```swift
protocol JSONValue {
  #if true
  func isEqual(to: JSONValue) -> Bool
  #endif
}
extension Array: JSONValue where Element: JSONValue {}
extension Dictionary: JSONValue where Value: JSONValue {}
extension String: JSONValue {}
extension Date: JSONValue {}
extension Int: JSONValue {}
extension Bool: JSONValue {}
extension Double: JSONValue {}
extension NSNull: JSONValue {}
extension AnyHashable: JSONValue {}
extension NSArray: JSONValue {}
extension NSDictionary: JSONValue {}

#if true
extension JSONValue where Self: Equatable {
  func isEqual(to: JSONValue) -> Bool {
    guard let other = to as? Self else { return false }
    return self == other
  }
}

// Might be worth duplicating these for NSArray/NSDictionary as well.
// I didn't think through all the edge-cases...
extension Array where Element: JSONValue {
  func isEqual(to: JSONValue) -> Bool {
    guard let other = to as? Array<JSONValue>,
          count == other.count else { return false }
    return (0..<count).allSatisfy { self[$0].isEqual(to: other[$0]) }
  }
}
extension Dictionary where Value: JSONValue {
  func isEqual(to: JSONValue) -> Bool {
    guard let other = to as? Dictionary<Key, JSONValue>,
          count == other.count else { return false }
    return allSatisfy { (k, v) in other[k].map { v.isEqual(to: $0) } ?? false }
  }
}
#endif

```

A quick test:

```swift
let testJSON =
    """
    {
      "test": [1, 2, 3],
      "another": "99"
    }
    """
    let data = testJSON.data(using: .utf8)!

    let obj = try! JSONSerialization.jsonObject(with: data, options: [])
	let jsObj = obj as! JSONValue
    assert(jsObj.isEqual(to: jsObj))

    let obj2 = try! JSONSerialization.jsonObject(with: data, options: [])
    let jsObj2 = obj2 as! JSONValue
    assert(jsObj.isEqual(to: jsObj2))

    let testJSON2 =
    """
    {
      "test": [4, 5, 6],
      "another": "66"
    }
    """
    let data2 = testJSON2.data(using: .utf8)!
    let obj3 = try! JSONSerialization.jsonObject(with: data2, options: [])
    let jsObj3 = obj3 as! JSONValue
    assert(jsObj.isEqual(to: jsObj3) == false)

```

---

<div class="post-metadata">

**Author:** ![NSExceptional](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/nsexceptional/32/6813_2.png) [@NSExceptional](https://forums.swift.org/u/NSExceptional)\
**Post date:** [May 15, 2019, 11:33pm UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/5 "2019-05-15T23:33:29Z")

</div>

> What you propose (OpaqueEquatable) is also a kind of type erasure.

Ah, yes. I almost referred to it as such in the post because that's what I thought too, but didn't want to confuse anyone if that wasn't quite right.

> I'm not sure what you mean by "by hand"

As opposed to not needing to type-erase things at all to compare them when the compiler doesn't enforce same-type comparison, like so:

```swift
let a: OpaqueEquatable = 5
let b: OpaqueEquatable = 5
a == b

```

> If it's just for testing

It's not just for testing, this was just an example. There are real-world use-cases for this that shouldn't require workarounds.

Your type-erasing example is clever, but, I did address it after my example when I said _"Type-erasure by hand could help here, maybe, but it's a very hacky solution to something the language itself should support."_ And I think that holds true here. You need to implement massive boilerplate for every collection you might want to compare, and you have to do it all again every project you wish to do this in.

I don't think we should be recommending this sort of thing; we should focus on patching this hole in the language.

---

<div class="post-metadata">

**Author:** ![anandabits](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/anandabits/32/355_2.png) [@anandabits](https://forums.swift.org/u/anandabits)\
**Post date:** [May 16, 2019, 2:13am UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/6 "2019-05-16T02:13:32Z")

</div>

> [@NSExceptional](#):
>
> we should focus on patching this hole in the language.

As I said previously, this is possible to implement in Swift today. If there is enough support for the idea it wouldn’t be too difficult to get a proposal put together with an implementation that introduces an API for dynamically comparing two unconstrained existential values.

Instead of focusing on mechanics, I recommend focusing on identifying the exact API you want to have available. There are several reasonable variations on semantics for the comparison. For example:

```swift
/// - returns: `true` when both values are of the same type that conforms to `Equatable` and compare equal.  
/// `false` if that comparison fails, and `nil` if the values are of different or non-`Equatable` types.
func areEqual(_ lhs: Any, _ rhs: any) -> Bool?

/// - returns: `true` when both values are of the same type that conforms to `Equatable` and compare equal.  
/// `false` otherwise.
func areKnownEqual(_ lhs: Any, _ rhs: any) -> Bool

/// - returns: `true` when both values are of different types or of the same type that conforms 
/// to `Equatable` and compare not equal. `false` otherwise.
func areKnownDifferent(_ lhs: Any, _ rhs: any) -> Bool

```

What are the desired semantics?

---

<div class="post-metadata">

**Author:** ![rex-remind](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/rex-remind/32/449_2.png) [@rex-remind](https://forums.swift.org/u/rex-remind)\
**Post date:** [May 16, 2019, 4:12am UTC](https://forums.swift.org/t/dynamic-equality-checking-and-equatable/24556/7 "2019-05-16T04:12:34Z")

</div>

Don't want to dismiss this idea outright but I think there need to be stronger use cases. Your first example makes me wonder why you're comparing those two arrays to begin with. Your second can be solved using algebraic data types or at a minimum can use what's stated here [Adding a polymorphic Equatable? - #2 by gwendal.roue](https://forums.swift.org/t/adding-a-polymorphic-equatable/12034/2)

Explicitly, your JSON example can be solved by relying instead on algebraic data types like many open source JSON libraries already offer. This blog post looks like it gives an example [Parsing fields in Codable structs that can be of any JSON type | by Sergio Schechtman Sette | Grand Parade | Medium](https://medium.com/grand-parade/parsing-fields-in-codable-structs-that-can-be-of-any-json-type-e0283d5edb)

Edit for clarity: Conforming `enum JSONValue: Equality` from that blog example should work since all associated values are Equatable.
