Add compactMapValues to Dictionary

Hi,

I’d like to propose about compactMapValues in Dictionary.

When I imagine removing nil value from dictionary at first as below, but it doesn’t works.

["1" :  "1", "2" : nil, "3" :  "3"].flatMap { $0 }

// [(key: "2", value: nil), (key: "1", value: Optional("1")), (key: "3", value: Optional("3"))]

To avoid this, I’ve heard using reduce like this.

let result = ["1": "1", "2": nil, "3": "3"].reduce(into: [String: String]()) { (result, x) in
    if let value = x.value { result[x.key] = value }
}

// ["1": "1", "3": "3"]

However now that we have Dictionary.mapValues() from Swift 4 and the Sequence.flatMap() will be renamed to Sequence.compactMap() with [SE-0187] . I want compactMapValues to remove nil value in dictionary as below.

["1" :  "1", "2" : nil, "3" : "3"].compactMapValues({ $0 })

// ["1": "1", "3": "3"]

How do you think that?

13 Likes

I think this idea simply makes sense, given the existence of Dictionary.mapValues and Sequence.compactMap.

I have been implementing such extensions myself so I'd be happy to see stdlib provide it by default.

1 Like

/cc @Karoly_Lorentey

I can see how this would be useful; it seems like a reasonable addition to complete the API. This is a different operation than mapValues(_:) followed by filter(_:).

Do you have a specific implementation in mind? (I suspect it might be possible to use Dictionary internals to eliminate rehashing in some cases. But it's probably better to just build a brand new Dictionary from scratch, like you do with reduce.)

1 Like

I can think of 2 implementations. 1) using reduce(into:_:) as posted above; and 2) Dictionary(uniqueKeysWithValues: srcDict.lazy.filter { $0.1 != nil }). They are not-really-quite-straightforward, perhaps, but neither benefits from being in the standard library from the performance standpoint. So I'm in the fence about including it.

Upd: and just to contradict myself, the .lazy.filter variant is incorrect as it does not unwrap the optionals. So, one extra point to being included.

2 Likes

I think making new Dictionary as I posted is better in viewing from performance, but I only don't have any good idea to use former, so feel free to post better idea if somebody has.

That works! Dictionary.filter(_:) doesn't try to be clever with hash table internals, either.

I think we should have more practical examples to motivate inclusion. Dictionaries of optional values do not occur very often in my experience -- I'd typically just declare a non-optional value and filter out nils during insertion. For other cases, a filter followed by mapValues would often work just as well as compactMapValues.

We added a compact method to Dictionary when I was at Kickstarter and used it throughout:

I even recently added a filterMapValues (and filteredValues) to our code base at Point-Free:

We try to avoid mutation and write code that's declarative, so our uses tend to be where we want to pass a dictionary literal to a call site that expects non-optional values, but where some of our inputs may be optional.

These use cases may be minor enough to not require an implementation in the standard library, but we'll probably continue to add our own extension and benefit from it regardless. It's a bit messy to filter and mapValues everywhere (especially with the force unwrap). What we want is succinctness and safety.

2 Likes

This isn’t quite so much about dictionaries with optional values as transformations of a dictionary’s values that yield an optional. For example:

let d = ["a": "1", "b": "2", "c": "three"]
// This is .compactMapValues(Int.init):
let e = d.mapValues(Int.init).filter({ $0.value != nil }).mapValues({ $0! })

There’s no real savings here from a hashing perspective—the filtering means we’ll need to rehash all the elements—but it would save on allocating more space than we need for a bunch of nil values.

5 Likes

Thank you Nate, I think it's more practical and frequency usecase. I believe compactMapValues is useful for us.

Then, what should I do next? I have a chance to write proposal, right? Anything idea do you have?

How about something like this as the implementation?

extension Dictionary {
    func compactMapValues<U>(_ transform:(Dictionary.Value)throws->U?)rethrows -> [Dictionary.Key:U] {
        var result:[Key:U] = [:]
        for (k,v) in self {
            if let newValue = try transform(v) {
                result[k] = newValue
            }
        }
        return result
    }
}
1 Like

Yes, I think this is worth an implementation and a small proposal. We've seen clear examples of its usefulness.

We have to consider the cognitive weight of adding too many APIs; however, we already have filter and mapValues, and they feel incomplete without compactMapValues -- leaving it out would be jarring.

3 Likes

OK! I'll try it! Thank you for you advise!

1 Like

I noticed the Swift Package Manager also defines its own version of compactMapValues:

https://github.com/apple/swift-package-manager/blob/master/Sources/Basic/DictionaryExtensions.swift#L30-L38

1 Like

Thank you lorentey!

I test performance this with my implementation as below

extension Dictionary {
    public func compactMapValues<T>(_ transform: (Value) throws -> T?) rethrows -> [Key: T] {
        return try self.reduce(into: [Key: T](), { (result, x) in
            if let value = try transform(x.value) {
                result[x.key] = value
            }
        })
    }
}

Then I found latter is more faster so that I'll implement by mine and try to fix SPM too.

I posted the proposal.

Thank you for your helping!!

2 Likes

I like the addition a lot, thanks for pushing this forward.

1 Like

I support this idea. I just wanted to write flatMapValues in the current Swift version that was renamed to compactMap(Values) on a dictionary to map the values by doing a dynamic cast as? on the values.

1 Like

Now that the proposal and implementation was approved by @lorentey. https://github.com/apple/swift/pull/15017

I cannot wait for starting evolution discussion!