Hilton Campbell
Hilton Campbell

Reputation: 6095

How do I write a generic Swift extension to deep merge two dictionaries?

I'm trying to write a function to deep merge two dictionaries. By this I mean that when the values for a key in both dictionaries are being merged and those values are both dictionaries, they will be merged instead of one being replaced by the other.

Here's what I have:

extension Dictionary {
    public func deepMerged(with other: [Key: Value]) -> [Key: Value] {
        var result: [Key: Value] = self
        for (key, value) in other {
            if let value = value as? [Key: Value], let existing = result[key] as? [Key: Value] {
                result[key] = existing.deepMerged(with: value)
            } else {
                result[key] = value
            }
        }
        return result
    }
}

But unfortunately it doesn't compile. I get the error

Cannot convert value of type '[Key : Value]' to expected argument type [_ : _]' on the use of value in the recursive call to `deepMerged(with:).

I'm able to work around this by scoping the extension:

extension Dictionary where Key == String, Value == Any {
    ...
}

This works for my particular use case at this time, but I don't understand why the more generic code doesn't work.

Upvotes: 1

Views: 273

Answers (1)

NobodyNada
NobodyNada

Reputation: 7634

Note that the error is on the assignment to result[key], not on the function call itself:

let merged: [Key:Value] = existing.deepMerged(with: value)  //works fine
result[key] = merged                                        //error

The compiler knows that:

  • result is a [Key: Value]
  • other is a [Key: Value]
  • key is a Key
  • value is a Value
  • merged is a [Key:Value]
  • result has a subscript(key: Key) -> Value? { get set } (i.e. a writable subscript of type Value? which accepts a Key)

But it does not know that [Key:Value] is a Value, so it does not know that merged can be passed to the subscript of result. Since value is a Value and value is also [Key: Value], we know that [Key: Value] must be a Value, but the compiler does not know that.

A solution is to cast merged to a Value:

extension Dictionary {
    public func deepMerged(with other: [Key: Value]) -> [Key: Value]
    {
        var result: [Key: Value] = self
        for (key, value) in other {
            if let value = value as? [Key: Value],
                let existing = result[key] as? [Key: Value],
                let merged = existing.deepMerged(with: value) as? Value
            {
                result[key] = merged
            } else {
                result[key] = value
            }
        }
        return result
    }
}

Upvotes: 3

Related Questions