SE-0218 — Introduce compactMapValues to Dictionary

Right, this is the problem: you can work around the lack of parameterized generics, but in a way where the method isn't properly constrained. It's best to avoid baking something like this into the ABI given how simple the composition from compactMap is. Once we have the ability to express it correctly, we can consider adding it.

Sorry, I realize I am partly responsible, but we should take this discussion to a separate thread. This thread should stick to discussing the current proposal.

6 Likes

I don’t love the name—it just reads like three unrelated words jammed together—but its logic flows straightforwardly from our existing methods, and I don’t have any alternative suggestions good enough to risk plunging this thread into a hundred posts of bikeshedding. Other than that, I think this proposal improves the language with almost no downside.

FWIW, it could be expressed if we introduced something along the lines of the Unwrappable protocol discussed here: Introducing `Unwrappable`, a biased unwrapping protocol. A protocol like that should be debated on its own merit of course, but it's worth pointing out that it would enable algorithms like these to work over types other than Optional (such as Result).

protocol Unwrappable {
    associatedtype Wrapped
    func unwrap() -> Wrapped?
}
extension Optional: Unwrappable {
    func unwrap() -> Wrapped? {
        return self
    }
}
extension Array where Element: Unwrappable {
    func compact() -> [Element.Wrapped] {
        return compactMap { $0.unwrap() }
    }
}
extension Dictionary where Value: Unwrappable {
    func comactValues() -> [Key: Value.Wrapped] {
        let result = lazy.compactMap { keyValue -> (Key, Value.Wrapped)? in
            guard let value = keyValue.value.unwrap() else {
                return nil
            }
            return (keyValue.key, value)
        }
        return Dictionary<Key, Value.Wrapped>(uniqueKeysWithValues: result)
    }
}
let array: [Int?] = [42, nil, 42]
let compactArray = array.compact()
let dictionary: [Int: String?] = [42: "42", 43: nil, 44: "44"]
let compactDictionary = dictionary.comactValues()
1 Like

Cool, does this means that we could even use a "semi-private" _OptionalProtocol to achieve the same goal?

Technically, I don't see why not. It isn't clear to me what the criteria are for using a technique like that though.

Note that _Unwrappable is already present in the standard library for bridging.

However, one principal use case for such a protocol relies on the fact that only Optional conforms to it. This is essentially the case here also: it's meant to be a workaround for not being able to write a parameterized extension. If other types could conform to the protocol, then it wouldn't be a workaround for this issue.

1 Like

Again with a mea culpa that I was part of the initial discussion (sorry!): please discuss compact on a separate thread, keeping this thread to discussion of the Dictionary.compactMapValues proposal. Thanks!

-1

The Swift Dictionary type does not naturally work with Optional values*.

Adding an API surface with a "difficult" name to supplement an uncommon use case seems frivolous.

(* it's obviously possible, but there's no way with the natural subscript API to distinguish between a missing value and a nil one, amongst other Dictionary APIs)

This isn't true.

let d: [String: Int?] = ["one": 1, "two": nil]

d["one"]   // .some(.some(1))
d["two"]   // .some(.none)
d["three"] // .none
2 Likes

The subscript returns nil in both cases:

(showing image of playground)

There are ways to peel back the nested Optional but they're not "natural", which is why I used that word.

This is just an issue with how the playground prints the value. There was a decision made at some point to automatically unwrap optionals before printing them in cases like this. I personally think it was a bad one, because it leads to confusion like this. Look at d["one"].debugDescription, or assign it to a variable and option-click the variable to see the type, and you'll see that it is Int?? not Int?. There is no theoretical trouble distinguishing between the missing value and the nil one, as they are .none and .some(.none) respectively, but it can be confusing.

3 Likes

Then let me use another example, store a nil value in a Dictionary through the normal subscript API.

d["four"] = nil

does not do that, you have do to something like:

d.updateValue(nil, forKey: "four")

(I personally think that code that uses .some and .none for Optionals is a smell)

Although I might well be the only one who voted against inclusion so far, I think your standpoint this is rather an argument for inclusion:
Due to how Optionals are modeled, they can be nested. I doubt that this is particular useful (but that's not up for debate), so if you read "does not naturally work with Optional values" as "try to avoid Optionals in Dictionaries", I agree.
As far as I understand it, this proposal is a way to avoid Dictionaries that contain Optionals - which you would create when using the existing map-function with a closure that returns an Optional.

1 Like

I'm typing this on my phone, so can't doublecheck, but either of these or a variation thereof ought to work:

d["four"] = .some(.none)
d["four"] = Optional(nil)

Also, note that this is specific to using a nil literal. In the case of a variable, that variable would be upgraded to optional (as any variable is) and inserted:

var d: [String:Int?] = [:]
d["four"] = Int("four")
print(d) // ["four": nil]
1 Like

Thanks everyone. The review has now ended and the core team has accepted the proposal.

6 Likes