kelan.io

Combining Dictionaries in Swift

Here we have 2 dictionaries that we want to combine.

// NOTE: This whole post is with Swift 1.2

let x = [1:2, 3:4]
let y = [5:6]

The other day, I asked about a Swift-y way to do this in our #swift Slack channel at work. Naturally, @jtbandes proposed a clever one-liner, using map() and Dictionary.updateValue():

var z = y
map(x, z.updateValue)  // But wait! Read on…
z  // => [5: 6, 4: 3, 2: 1]

I tried to generalize that in an extension on Dictionary.

extension Dictionary {

    /// Return a new dictionary with values from `self` and `other`.  For duplicate keys, self wins.
    func combinedWith(var other: Dictionary) -> Dictionary {
        // NOTE: This doesn't work
        map(self, other.updateValue)
        // ^~~  error: Cannot find an overload for 'map' that accepts an argument
        //      list of type '(Dictionary<Key, Value>, (Value, forKey: Key) -> Value?)'
        return other
    }
}

But if you look carefully at the output of the Int version above, the key and values are swapped (so it only happens to compile here because they’re both Ints). Furthermore, the compiler complains about my generalized version (see the error message above).

Looking closely at the signature for map():

func map<C : CollectionType, T>(source: C, transform: (C.Generator.Element) -> T) -> [T]

We can see that it wants a transform function that does Element -> T (where Dictionary has typealias Element = (Key, Value)). But updateValue()‘s signature is:

mutating func updateValue(value: Value, forKey key: Key) -> Value?

So, it takes (Value, Key). So, they don’t match up; the order of Key and Value is swapped.

It only worked for that first example because they were both Int. But the compiler was right to complain in the general case.

Flipping Things Around

So Jacob suggested transforming updateValues() to work how we want, by looking at it from a functional perspective.

We can write a function that takes as its input a function with two arguments in one order, and returns a function that takes them in the opposite order.

The fun part about Swift is that you can translate my previous sentence into a function signature:

func flipInputs<T,U,V>(f: (T,U) -> V) -> (U,T) -> V
//                     ^               ^~~~ (2) …and return a function that
//                     |                        takes them in the opposite order.
//                     |
//                     \~~~ (1) Take a function that takes arguments in 1 order…
//
// NOTE: The `V` isn't relevant to the argument flipping, but is necessary to
//       show that the new function returns the same type of thing as the old function.

And then, there is basically just one way you can implement that function, and again, the language/compiler walks you through it, and keeps you honest.

func flipInputsVerbose<T,U,V>(f: (T,U) -> V) -> (U,T) -> V {
    // We need a new function, so make one that takes two args, of type (T,U)…
    func flippedFunc(t: T, u: U) -> V {
        // …and returns the result of calling the input function with the args in
        // the opposite order.
        return f(u,t)
    }
    // Now just return that new function.
    return flippedFunc
}

Or we can shorten that to just:

func flipInputsConcise<T,U,V>(f: (T,U) -> V) -> (U,T) -> V {
    return { (u,t) in f(t,u) }
}

And then use that in the body of our extension method.

extension Dictionary {

    /// Return a new dictionary with values from `self` and `other`.  For duplicate keys, self wins.
    func combinedWith(var other: Dictionary) -> Dictionary {
        map(self, flipInputs(other.updateValue))
        return other
    }

    /// Mutating version.
    mutating func combineWith(other: Dictionary<Key,Value>) {
        self = self.combinedWith(other)
    }
}

So, back to our initial question:

let x = [1:2, 3:4]
let y = [5:6]

let combined = x.combinedWith(y)
y  // => [5: 6]  (y isn't modified)
combined  // => [5: 6, 3: 4, 1: 2]  // the key/values remain in the correct positions

var mutable = [1: 100]
mutable.combineWith(combined)  // => [5: 6, 3: 4, 1: 100]

// And it obviously works with other types of Dictionaries too:
let mixedA = [ "one": 1, "two": 2 ]
let mixedB = [ "three": 3, "one": 100 ]
mixedA.combinedWith(mixedB)  // => ["three": 3, "one": 1, "two": 2]

Boring Version

Instead of flip + updateValue, we could have also just use a for loop (which, if I’m being honest, is probably the clearest implementation).

extension Dictionary {
    /// Return a new dictionary with values from `self` and `other`.  For duplicate keys, self wins.
    func combinedWith(var other: Dictionary<Key,Value>) -> Dictionary<Key,Value> {
        for (key, value) in self {
            other[key] = value
        }
        return other
    }
}

But where’s the fun in that?