Daniel Daniel - 5 months ago 27
Swift Question

Generic function to merge two dictionaries

I'd like a generic function to merge two dictionaries. It needs to be more all-purpose then the one described in How do you add a Dictionary of items into another Dictionary. Reason: I have objects in my dictionaries, and I only want to update a specific property as opposed to the whole object itself. (To be concrete, they are objects with an array property. I need to either append to this array if the object exists or create a new object with a new array. I can't simply inspect and accordingly overwrite the whole object. I'm interested in it's property.)

I tried to do this using functional programming:

extension Dictionary {
func merge(withDictionary: Dictionary) -> Dictionary {
var returnDictionary = withDictionary // make a copy of dictionary (a dictionary is a struct in Swift)
// Merge self dictionary into returnDictionary
for key in self.keys {
// If there is already a value associated for this key. (In my concrete case I will need to append to the value object's list property.)
if let withDictionaryValue = returnDictionary[key], selfValue = self[key] {

// I NEED TO DO THIS HERE.
//
// returnDictionary[key]!.list = withDictionaryValue.list + selfValue.list
//
// CAN'T FIGURE OUT HOW TO DO THIS IN A GENERIC WAY. THIS GENERIC MERGE SHOULDN'T NEED TO KNOW THAT THIS PARTICULAR DICTIONARY HAS VALUE OBJECTS THAT CONTAIN A 'list' PROPERTY.
// HOW DO I PASS THIS CODE SNIPPET LINE IN AS PART OF A CLOSURE, WHICH USES 'withDictionaryValue' AND 'selfValue'?

} else {
// Simply write into this key - it doesn't yet contain values.
returnDictionary[key] = self[key]
}
}

return returnDictionary

}

}

Answer

This generic merge shouldn't need to know that this particular dictionary has value objects that contain a 'list' property.

On the contrary, in order for your function to access the list property on the dictionary's values, you need to tell the compiler that the values have a list property. You can do this by creating a protocol and constraining the extension to only operate on dictionaries that have values that conform to this protocol:

// your protocol that defines the list property
protocol ListType {
    var list : [AnyObject] { get set }
}

extension Dictionary where Value : ListType {
    func merge(withDictionary: Dictionary) -> Dictionary {
        var returnDictionary = withDictionary   // make a copy of dictionary (a dictionary is a struct in Swift)

        for (key, value) in self { // iterate through key value pairs
            if let withDictionaryValue = returnDictionary[key] { // if value exists, merge the list properties
                returnDictionary[key]!.list = withDictionaryValue.list + value.list
            } else {
                returnDictionary[key] = self[key]
            }
        }
        return returnDictionary
    }
}

You can then simply conform the value types you're using to this protocol either directly, or through an extension.

struct Foo : ListType {
    var list: [AnyObject]
}

let d = ["foo" : Foo(list: ["foo", "bar", "baz"])]
let d1 = ["foo" : Foo(list: ["qux", "blah", "blue"])]

let r = d.merge(d1) // ["foo": Foo(list: [qux, blah, blue, foo, bar, baz])]

If you want a more general approach, you could define a Mergable protocol that defines a method where the conforming type can do their own merging logic. You would then change your extension to call this method if the values conform to the protocol, else just merge the key-value pairs.

protocol Mergable {
    func merge(withOther:Self) -> Self
}

// if values are Mergable (and the key-value pairs exist in both dictionaries), then call their custom logic for merging
extension Dictionary where Value : Mergable {
    func merge(withDictionary: Dictionary) -> Dictionary {
        var returnDictionary = withDictionary
        for (key, value) in self {
            if let withDictionaryValue = withDictionary[key] {
                returnDictionary[key] = value.merge(withDictionaryValue)
            } else {
                returnDictionary[key] = self[key]
            }
        }
        return returnDictionary
    }
}

// standard merging logic
extension Dictionary {

    func merge(withDictionary: Dictionary) -> Dictionary {
        var returnDictionary = withDictionary
        keys.forEach {returnDictionary[$0] = self[$0]}
        return returnDictionary
    }
}

You could then conform your value type to this protocol like so:

// Foo's custom merging logic
extension Foo : Mergable {
    func merge(withOther: Foo) -> Foo {
        var merged = self

        // merge the list array
        merged.list.appendContentsOf(withOther.list)

        return merged
    }
}

If your dictionary's values are Mergable, then the compiler will favour the extension that does the custom merging logic, as the more type specific signature is preferred.


In reply to your comment about wanting to do this with closures, you could add a closure argument to the merge function that will take both values and return a merged value.

extension Dictionary{
    func merge(withDictionary: Dictionary, @noescape merge: (Value, Value) -> Value) -> Dictionary {
        var returnDictionary = withDictionary // make a copy of dictionary (a dictionary is a struct in Swift)

        for (key, value) in self { // iterate through key value pairs
            if let withDictionaryValue = returnDictionary[key] {
                returnDictionary[key] = merge(value, withDictionaryValue) // invoke closure to merge values
            } else {
                returnDictionary[key] = self[key]
            }
        }
        return returnDictionary
    }
}

// call merge with our own custom merging logic when a given key exists in both dictionaries
let result = dictA.merge(dictB) {
    var returnValue = $0
    returnValue.list.appendContentsOf($1.list)
    return returnValue
}

Although this only really makes sense if your merging logic changes each time you call the function, which I suspect is not what you're doing.

If you want your merging logic to remain constant, then this better done with protocols. With closures you're going to have to pass in your merging logic every time you want to invoke the function. With protocols, you just define that logic once – and it's invoked when needed.

Comments