- Proposal: SE-0218
- Author: Daiki Matsudate
- Review Manager: Ben Cohen
- Status: Implemented (Swift 5.0)
- Implementation: apple/swift#15017
- Decision Notes: Rationale
This proposal adds a combined filter/map operation to Dictionary
, as a companion to the mapValues
and filter methods introduced by SE-0165. The new compactMapValues operation corresponds to compactMap on Sequence.
- Swift forums pitch: Add compactMapValues to Dictionary
Swift 4 introduced two new Dictionary
operations: the new method mapValues
and a new version of filter
. They correspond to the Sequence
methods map
and filter
, respectively, but they operate on Dictionary
values and return dictionaries rather than arrays.
However, SE-0165 left a gap in the API: it did not introduce a Dictionary
-specific version of compactMap
. We sometimes need to transform and filter values of a Dictionary
at the same time, and Dictionary
does not currently provide an operation that directly supports this.
For example, consider the task of filtering out nil
values from a Dictionary
of optionals:
let d: [String: String?] = ["a": "1", "b": nil, "c": "3"]
let r1 = d.filter { $0.value != nil }.mapValues { $0! }
let r2 = d.reduce(into: [String: String]()) { (result, item) in result[item.key] = item.value }
// r1 == r2 == ["a": "1", "c": "3"]
Or try running a failable conversion on dictionary values:
let d: [String: String] = ["a": "1", "b": "2", "c": "three"]
let r1 = d.mapValues(Int.init).filter { $0.value != nil }.mapValues { $0! }
let r2 = d.reduce(into: [String: Int]()) { (result, item) in result[item.key] = Int(item.value) }
// r == ["a": 1, "b": 2]
While mapValues
and filter
can be combined to solve this tasks, the solution needs multiple passes on the input dictionary, which is not particularly efficient. reduce(into:)
provides a more efficient solution, but it is rather tricky to get right, and it obscures the intended meaning of the code with implementation details.
It seems worth adding an extra extension method to Dictionary
for this operation; its obvious name is compactMapValues(_:)
, combining the precedents set by compactMap
and mapValues
.
let r3 = d.compactMapValues(Int.init)
Add the following to Dictionary
:
let d: [String: String?] = ["a": "1", "b": nil, "c": "3"]
let r4 = d.compactMapValues({$0})
// r4 == ["a": "1", "c": "3"]
Or,
let d: [String: String] = ["a": "1", "b": "2", "c": "three"]
let r5 = d.compactMapValues(Int.init)
// r5 == ["a": 1, "b": 2]
Add the following to Dictionary
:
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
}
})
}
}
This change is purely additive so has no source compatibility consequences.
This change is purely additive so has no ABI stability consequences.
This change is purely additive so has no API resilience consequences.
We can simply omit this method from the standard library -- however, we already have mapValues
and filter
, and it seems reasonable to fill the API hole left between them with a standard extension.