Dictionary merging function with default uniquingKeys

Swift standard library has some functions to merge 2 dictionaries.
https://developer.apple.com/documentation/swift/dictionary/3127171-merge
https://developer.apple.com/documentation/swift/dictionary/3127175-merging

mutating func merge<S>(_ other: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows where S : Sequence, S.Element == (Key, Value)
func merging<S>(_ other: S, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows -> [Key : Value] where S : Sequence, S.Element == (Key, Value)

I feel having to specify this combine closure every time I call this function is too verbose. Imagine a code base with a lot dictionary merging, you will see a lot of repeating combine closures that either select current or new value for duplicated keys. Like this:

let keepingCurrent = dictionary.merging(newKeyValues) { (current, _) in current }
let replacingCurrent = dictionary.merging(newKeyValues) { (_, new) in new }

I know that having a combine closure is useful because it can handle cases where you might not want to choose either current or new values, but to create a completely new value from the two. But I still think it is too verbose for simple use cases that I want to just either pick one of the values.

What I'm proposing is there should be another function dictionary merging/updating function that automatically pick one of the duplicated keys without specifying a combining closure. I think this way is more concise and readable.

I'm throwing some ideas of the function name may look like.

For updating keeping current keys:

let keepingCurrent = dictionary.updateByKeepingCurrent(using: newKeyValues)
let keepingCurrent = dictionary.updateIfNew(using: newKeyValues)
let keepingCurrent = dictionary.addIfNotExist(using: newKeyValues)

For updating replacing current keys:

let replacingCurrent = dictionary.updateByReplacingCurrent(using: newKeyValues)
let replacingCurrent = dictionary.update(using: newKeyValues)
let replacingCurrent = dictionary.addOrReplace(using: newKeyValues)

Another idea is to just make dictionary types support binary operators like + and +=. But one may argue that this is too far of an approach and is prone to correctness and safety. Because the detail of how the duplicated keys are resolved is not visible in these operators.

I feel this is such a fundamental feature that might had been discussed before and Swift team might already discarded or not prioritized yet. However, I can't find a reference to such discussions. Even if this idea is discarded, I will be happy to learn the reasons why.

Let me know what you guys think. :slight_smile:

2 Likes

I think if there was an addition I would prefer something like:

enum DictionaryMergeStrategy {
case keepOldValue
case useNewValue
}

dictionary.merging(strategy: .keepOldValue)
3 Likes

The improved type checker should allow you to do this:

let mergeOld = values.merge(other) { $0 }
let mergeNew = values.merge(other) { $1 } // already works

Whether this level of terseness is desirable is another story.

4 Likes

I know about this. But I think this $0 & $1 is not very intuitive.

Maybe just one method update(with:) that would overwrite the values of the receiver with the values from the parameters. If you want to keep the old values you would call it the other way around:

oldValues.update(with: newValues) // Overwrite with new values
newValues.update(with: oldValues) // Keep the old values
3 Likes

Good point. This should work well with the non-mutating version of the update function.

None of this is a good use of time to implement in the existing standard library, and it will be cruft when the language gets better. The feature you actually want is to have definable default argument names for closure arguments, à la newValue and oldValue in property observers.

Until that happens, I'll continue using dollar signs for this use case. $1 works here as well as anywhere else it's allowed. It's not good, but it's good that it's bad. Because it will motivate getting the change in that I mentioned.

1 Like
  1. The parentheses around the parameters in your closures aren't necessary, so you could just do:

    d1.merging(d2, uniquingKeysWith: { _, new in new })
    d1.merging(d2, uniquingKeysWith: { old, _ in old })
    
  2. The params old/new work better than current/new, because they're both the same length

  3. You can extract the closures into named functions, and use them in a really readible way:

    let d1 = [
      "a": 1,
      "b": 2,
      "c": 3,
    ]
    
    let d2 = [
      "b": 102,
      "c": 103,
      "d": 104
    ]
    
    func takeNewValue<V>(_ old: V, _ new: V) -> V { return new }
    func keepOldValue<V>(_ old: V, _ new: V) -> V { return old }
    
    print(d1.merging(d2, uniquingKeysWith: takeNewValue))
    print(d1.merging(d2, uniquingKeysWith: keepOldValue))
    

I wouldn't suggest wrapping the merging call itself, because it's well understood by other programmers, and wrapping it would only obscure it.

As the saying goes, C programmers under-use dictionaries (because there isn't a dictionary type built into the standard library, and it's quite difficult to write a generic, universally decent dictionary in C), but Ruby/Python programmers over-use dictionaries (because they're too accessible). I'm curious what made you need to merge dictionaries in so many places. I've certainly run into many use-cases, but not so many that a closure's syntax becomes to heavy. Perhaps you're over-using dicts, or mutating them in strange ways?

1 Like

I was referring to the $0 case, since that wouldn’t work prior to the type-checker update in Swift 5.3.

$0 doesn't work in Xcode 12 beta 6. :crying_cat_face:

/// Return an unmodified value when uniquing `Dictionary` keys.
public enum PickValue<Value> { }

public extension PickValue {
  typealias Original = Value
  typealias Overwriting = Value

  /// Keep the original value.
  static var keep: (Original, Overwriting) -> Value {
    { original, _ in original }
  }

  /// Overwrite the original value.
  static var overwrite: (Original, Overwriting) -> Value {
    { $1 }
  }
}
let original = ["🗝": "🏰"]
let overwriting = ["🗝": "✍️"]

XCTAssertEqual(
  original.merging(overwriting, uniquingKeysWith: PickValue.keep),
  original
)

XCTAssertEqual(
  original.merging(overwriting, uniquingKeysWith: PickValue.overwrite),
  overwriting
)

Hmm, I misremembered it (Function builders implementation progress).

Oh really? This is sad :frowning:

I like this idea :slight_smile: