SE-0218 — Introduce compactMapValues to Dictionary

I agree, it would seem really strange to see a call to a method with map in its name and not see a transform. If we want this behavior directy there should be a separate method named compactValues.

3 Likes

Point taken and Agreed.
If anything the implementation for these could be .compactMap(id) and .compactMapValues(id) -- where id is most likely replaced by the KeyPath equivalent of id.

1 Like

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) }
    }
  }
}
3 Likes

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?
7 Likes

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".)

3 Likes

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)
Terms of Service

Privacy Policy

Cookie Policy