Swift Digest
SE-0218 | Swift Evolution

Introduce compactMapValues to Dictionary

Proposal
SE-0218
Authors
Daiki Matsudate
Review Manager
Ben Cohen
Status
Implemented (Swift 5.0)

01 何が問題だったのか

Swift 4 では SE-0165 によって、DictionarymapValues と新しい filter が導入されました。これらは Sequencemap / 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]

mapValuesfilter を組み合わせる書き方は、Dictionary を複数回走査することになり効率がよくありません。reduce(into:) を使えば一度のパスで済みますが、書き方が込み入っていてコードの意図が実装の細部に埋もれてしまいます。

Sequencemap / filter に対して compactMap があるように、DictionarymapValues / filter に対しても「変換と nil 除去を同時に行う」メソッドがあってしかるべき、というのがこの proposal の問題意識です。

02 どのように解決されるのか

DictionarycompactMapValues(_:) メソッドが追加されました。バリューに変換クロージャを適用し、結果が 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 にはキーごと含まれません。

SequencecompactMap と同じ感覚で、Dictionary の「変換 + nil 除去」を素直に表現できるメソッドです。