SE-0218 — Introduce compactMapValues to Dictionary

This was discussed at the time of the name change of compactMap. compact cannot be expressed in the generics system yet. In fact it's used, under the name someValues, as the motivating example for parameterized extensions in the generics manifesto.

Luckily we don't have to debate it, because it isn't a possibility. You can't default a generic argument in that way. Try it by compiling the following:

func id<T>(_ t: T) -> T { return t }

extension Sequence {
  func cMap<T>(_ transform: (Element)->T? = id) -> [T] {
    return reduce(into: []) {
      if let x = transform($1) { $0.append(x) }

It is not the official goal.

There are temporary practical reasons prior to ABI stability for keeping the standard library small, because it must be shipped with every app. Once the library ships with the OS, this is no longer a concern.

Method proliferation can be a problem. If every type is bristling with overlapping or barely-useful methods, the API becomes bewildering and hard to use. This is more of a concern when extending broad protocols like Collection, less so when extending concrete types like Dictionary.

Consistency is also a mitigating factor here. If there is a map on Sequence and a mapValues on Dictionary, then it is helpful to users working with the API to follow through with compactMap and compactMapValues.

Obviousness of implementation is not necessarily a reason to exclude an addition. There are many methods in the standard library that have an obvious implementation. "Trivially composable" has been used as the term for avoiding gratuitous additions – but the word trivial is important here. It refers to compositions such as !isEmpty rather than operations that require a for loop or multiple compound statements.

Some criteria to consider for an addition of a method to the standard library are:

  • does it aid readability?
  • is it a common operation?
  • is it trivially composable?
  • is it consistent with existing methods?
  • does it help avoid a correctness trap?
  • does it help avoid a performance trap?
  • alternatively: might it encourage misuse?
  • can it be more efficiently implemented internally within the std lib?

It's trivial considering id works with .compactMap but if we add an additional criteria point: (lower the bar for newcomers); then surely there's merit?

I may be missing something, but this extension seems to work just fine:

extension Sequence {
    func compact<Dummy>() -> [Dummy] where Element == Dummy? {
        return compactMap({$0})

let xs = [1, 2, nil, 4, 5]
let ys = xs.compact()
print(ys)  // [1, 2, 4, 5]

(One definite usability downside is ys.compact() will still show up in autocompletion and the resulting error is rather unfriendly: "Generic parameter 'Dummy' could not be inferred".)


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.


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!


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

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.


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.