Introduce compactMapValues to Dictionary
01 何が問題だったのか
Swift 4 では SE-0165 によって、Dictionary に mapValues と新しい filter が導入されました。これらは Sequence の map / filter に相当しますが、Dictionary のバリューに対して働き、結果として配列ではなく Dictionary を返します。
しかし SE-0165 は、Sequence における compactMap に相当する Dictionary 専用メソッドを導入しませんでした。実際のコードでは、バリューに変換をかけつつ nil を取り除きたい場面は珍しくありません。
たとえば、Optional を含む Dictionary から nil を取り除きたいケースです。
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"]
あるいは、バリューに対して失敗し得る変換を適用したいケースです。
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)
}
// r1 == r2 == ["a": 1, "b": 2]
mapValues と filter を組み合わせる書き方は、Dictionary を複数回走査することになり効率がよくありません。reduce(into:) を使えば一度のパスで済みますが、書き方が込み入っていてコードの意図が実装の細部に埋もれてしまいます。
Sequence の map / filter に対して compactMap があるように、Dictionary の mapValues / filter に対しても「変換と nil 除去を同時に行う」メソッドがあってしかるべき、というのがこの proposal の問題意識です。
02 どのように解決されるのか
Dictionary に compactMapValues(_:) メソッドが追加されました。バリューに変換クロージャを適用し、結果が nil のエントリを取り除いた新しい Dictionary を返します。キーはそのまま維持されます。
基本的な使い方
Optional を含む Dictionary から nil を取り除くには、恒等クロージャを渡すだけで済みます。
let d: [String: String?] = ["a": "1", "b": nil, "c": "3"]
let r = d.compactMapValues({ $0 })
// r == ["a": "1", "c": "3"]
// r の型は [String: String]
バリューに失敗し得る変換を適用したい場合も、Int.init のような failable イニシャライザをそのまま渡せます。
let d: [String: String] = ["a": "1", "b": "2", "c": "three"]
let r = d.compactMapValues(Int.init)
// r == ["a": 1, "b": 2]
// "three" は Int(_:) が nil を返すので除外される
mapValues + filter で書くときのように中間の Dictionary を作らず、1 回のパスで変換と除去が完了します。
シグネチャ
標準ライブラリには次のような定義で追加されます。
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
}
}
}
}
ポイントは次のとおりです。
- ジェネリックパラメータ
Tは変換後のバリュー型で、元のValueとは独立に決められます。 - クロージャは
throws可能で、メソッド自体はrethrowsなので、投げないクロージャを渡せば呼び出し側もtryを書く必要はありません。 - クロージャが
nilを返したエントリはスキップされ、結果のDictionaryにはキーごと含まれません。
Sequence の compactMap と同じ感覚で、Dictionary の「変換 + nil 除去」を素直に表現できるメソッドです。