Search code examples
swiftsequenceextension-methodsequatable

Extension for sequences of dictionaries where the values are Equatable


I tried to implement the following method to remove double entries in an array of dictionaries by comparing their specific keys. However, this extension method will not work due to the error:

Binary operator == cannot be applied to two 'Equatable' operands

These are obviously equatable and same type (Iterator.Element.Value), so why doesn't it work?

I see that it treats Equatable as a specific type, not a constraint. I could not make it work with generic type or by writing where Iterator.Element == [String: Any], Iterator.Element.Value: Equatable.

Do you guys have any clues about how to solve this?

extension Sequence where Iterator.Element == [String: Equatable] {
    public func removeDoubles(byKey uniqueKey: String) -> [Iterator.Element] {
        var uniqueValues: [Iterator.Element.Value] = []
        var noDoubles: [Iterator.Element] = []
        for item in self {
            if let itemValue = item[uniqueKey] {
                if (uniqueValues.contains { element in
                    return itemValue == element
                }) {
                    uniqueValues.append(itemValue)
                    noDoubles.append(item)
                }
            }
        }
        return noDoubles
    }
}

Solution

  • A [String: Equatable] is a mapping of strings to any Equatable type. There is no promise that each value be the same equatable type. That said, it's not actually possible to create such a dictionary (since Equatable has an associated type), so this extension cannot apply to any actual type in Swift. (The fact that you don't receive an error here is IMO a bug in the compiler.)

    The feature you'd need to make this work is SE-0142, which is accepted, but not implemented. You currently cannot constrain an extension based on type constraints this way.

    There are many ways to achieve what you're trying to do. One straightforward way is to pass your equality function:

    extension Sequence {
        public func removeDoubles(with equal: (Iterator.Element, Iterator.Element) -> Bool) -> [Iterator.Element] {
            var noDoubles: [Iterator.Element] = []
            for item in self {
                if !noDoubles.contains(where: { equal($0, item) }) {
                    noDoubles.append(item)
                }
            }
            return noDoubles
        }
    }
    
    let noDupes = dict.removeDoubles(with: { $0["name"] == $1["name"] })
    

    This is slightly different than your code in how it behaves when name is missing, but slight tweaks could get what you want.

    That said, the need for this strongly suggests an incorrect data model. If you have this sequence of dictionaries, and you're trying to build an extension on that, you almost certainly meant to have a sequence of structs. Then this becomes more straightforward. The point of a dictionary is an arbitrary mapping of keys to values. If you have a small set of known keys that are legal, that's really a struct.