Dictionary.mapKeyedValuesを導入する
Introduce Dictionary.mapKeyedValues
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
Dictionary の値を、キーを使って変換したい場面はよくあります。たとえば通貨コードをキーに残高を値として持つ辞書に対し、「USD balance: 13」のような表示用文字列に変換したいといったケースです。
ところが Dictionary.mapValues(_:) はクロージャに値しか渡さないため、キーを参照したい変換では従来 init(uniqueKeysWithValues:) や reduce(into:) に頼るしかありませんでした。
// いずれもキーを使いたいがために遠回りになる
let new: [Key: NewValue] = .init(
uniqueKeysWithValues: old.lazy.map { ($0, transform(id: $0, payload: $1)) }
)
let new: [Key: NewValue] = old.reduce(into: [:]) {
$0[$1.key] = transform(id: $1.key, payload: $1.value)
}
これらはいずれも、変換後の辞書をゼロから構築し直す形になります。Dictionary は内部的にハッシュテーブルを持つため、同じキー集合をそのまま引き継げばよい場面でも、キーごとに再ハッシュと挿入処理が発生します。reduce(into:) の方は途中でストレージの再確保も挟まるぶんさらに不利で、キーを一切変更しない用途に対しては明らかに過剰なコストがかかっていました。
mapValues(_:) であれば元のハッシュテーブル構造をそのまま再利用でき、こうしたコストは発生しません。キーを参照したいというだけの理由でその最適化を諦めなければならない状態は、素直ではありません。
02 どのように解決されるのか
Dictionary に、クロージャがキーと値の両方を受け取る mapKeyedValues(_:) と compactMapKeyedValues(_:) を追加します。既存のキーをそのまま流用するため、ハッシュテーブルの再構築は不要で、値の変換だけが走ります。
extension Dictionary {
public func mapKeyedValues<T, E>(
_ transform: (Key, Value) throws(E) -> T
) throws(E) -> Dictionary<Key, T>
public func compactMapKeyedValues<T, E>(
_ transform: (Key, Value) throws(E) -> T?
) throws(E) -> Dictionary<Key, T>
}
compactMapKeyedValues は当初の提案には含まれておらず(reduce でも書けるうえ、mapKeyedValues のようなハッシュテーブル再利用による性能上の利点が得られないため)代替案として触れられていただけでしたが、それでも有用な操作であり、名前を与える価値があるとレビューで判断され、受理時に追加されました。
使い方は、mapValues / compactMapValues に渡すクロージャの引数を2つ(キーと値)にした形です。
let balances: [Currency: Int64] = [.USD: 13, .EUR: 15]
// キーを使って表示用文字列に変換
let displayText: [Currency: String] = balances.mapKeyedValues { key, value in
"\(key.alpha3) balance: \(value)"
}
// キーに応じて値を捨てる/残す
let positiveUSD: [Currency: Int64] = balances.compactMapKeyedValues { key, value in
key == .USD && value > 0 ? value : nil
}
mapKeyedValues は変換後もキーが必ず元の辞書と同じなので、Dictionary はキーの配列とハッシュテーブルをそのまま引き継ぎ、値の配列だけを作り直します。init(uniqueKeysWithValues:) や reduce(into:) による書き換えで発生していた再ハッシュ・再確保のコストがなくなり、キーを使わない既存の mapValues と同じオーダーで動きます(compactMapKeyedValues は結果のキー集合が変わり得るため、この最適化の恩恵は受けません)。
なぜ mapValues のオーバーロードではなく別名なのか
当初の草案では、既存の mapValues にキーと値を受け取るクロージャのオーバーロードを追加する案でした。しかしこれは、値がタプル型 (Key, Value) の辞書に対して既存の mapValues を呼んでいた稀なコードで、呼び出しが新しいオーバーロードに解決されてしまい、ソース互換性を壊すことが判明しました。そこで衝突を避けるため、mapKeyedValues という別名のメソッドとして追加する形に落ち着いています。
03 今後の見通し
mapValues という名前の付け替え
将来的に、既存の値のみを受け取る mapValues を mapValuesWithoutKeys のような名前へ改名し、mapValues という名前をキーと値を受け取る版(今回の mapKeyedValues に相当する形)に付け直す案が示されています。後続の言語モードで切り替えることを想定したもので、実現を約束するものではありません。
swift-collections の OrderedDictionary への展開
swift-collections の OrderedDictionary にも mapKeyedValues 相当を追加する案が、自然な拡張として挙げられています。OrderedDictionary はキー配列とは別にハッシュテーブルをサイドカーとして持っており、現状の reduce や init を使った書き換えではハッシュテーブル全体の再構築とキー配列のコピーが走ります。新しい方式ならキーのストレージを丸ごと再利用できるため、性能向上は Dictionary 以上に大きくなる可能性があります。構想段階のもので、実現を約束するものではありません。