currently, when it is needed to compute the mapped dictionary value using the dictionary key, it is required to do one of the following:
let new: [Key: NewValue] = .init(
uniqueKeysWithValues: old.lazy.map { ($0, transform(id: $0, payload: $1)) }
)
// or
let new: [Key: NewValue] = old.reduce(into: [:]) {
$0[$1.key] = transform(id: $1.key, payload: $1.value)
}
these are both highly pessimized patterns due to expensive hashing, although benchmarks frequently show that the first one is slightly “less bad” than the second one due to having fewer intermediate reallocations.
unlike in Mapping Dictionary keys , the intent here is never to modify the dictionary keys, the keys are only used to provide context (such as aggregation parameters) that is not part of the payloads.
why can’t there be a Dictionary.mapValues(_:) that avoids rehashing but supplies the key along with the value to the transformation function?
due-diligence LLM opinion
LLM used: Google Gemini 3 Pro
You are absolutely right to identify this as a "pessimized" pattern. The frustration stems from the fact that
Dictionaryinternally holds all the data required to perform this operation efficiently, but the public API simply does not expose a way to leverage it.There is no technical limitation in the hash table architecture preventing this; it is purely an API surface omission.
The Technical Bottleneck
To understand why you cannot implement a "Zero-Cost"
mapValuesWithKeyin user-space Swift, we have to look at howmapValuesworks internally versus the public initializers.
- Standard mapValues (Fast):The implementation of mapValues(_:) in the Standard Library does not iterate, hash, and insert. Instead, it performs a lower-level memory operation:
- It allocates a new storage buffer.
- It
memcpys the metadata (hash seeds, count).- It
memcpys the Keys and Hash/Control Bytes directly from the old storage to the new storage.- It then iterates the storage slots, applies the transform to the value, and initializes the new value memory.
- Result: O(n) operation with 0 hashes and linear memory access.
- init(uniqueKeysWithValues:) / reduce (Slow):Since user code cannot access the private _HashTable or raw storage buffers of a Dictionary, you are forced to treat the source dictionary as a Sequence.
- You extract
(Key, Value)pairs.- You initialize a new empty Dictionary.
- For every pair, you must re-hash the Key.
- You must perform linear probing to find the correct bucket in the new storage (even though it is mathematically guaranteed to be in the same relative slot if the capacity matches).
- Result: O(n) operation + O(n) hashes + O(n) probing sequences.
Why is this API missing?
There is no explicitly stated reason in the Swift source history, but it likely falls into two categories:
- API Minimalism: The original functional programming patterns for "Functors" usually imply that
maptransforms the content (value) without regard to the context (index/key).- Oversight:
mapValueswas introduced to solve the specific pain point of "I want to change values but keep keys." The "I need the key to calculate the value" use case is slightly rarer and likely just wasn't pitched during the original review.Is there a workaround?
If
NewValueis the same type asOldValue, you can achieve near-zero-cost performance by mutating in place, avoiding the reallocation of the hash/key buffer entirely:Swift
// Only efficient if ValueType == NewValueType var dict = old for index in dict.indices { let (key, value) = dict[index] // Access key/value directly from storage dict.values[index] = transform(id: key, payload: value) }However, if you are changing types (e.g.,
[String: Int]to[String: String]), there is no performant workaround in user code. You cannot allocate a Dictionary storage buffer with a newstride(for the new value type) and copy the control bytes yourself because those interfaces (_DictionaryStorage, etc.) are internal/private.Summary
You aren't missing a trick. The performance penalty you are paying is unavoidable without a change to the Swift Standard Library.
Would you like me to draft a pitch for this implementation on the Swift Evolution forums? It is a non-breaking, additive change that exposes existing internal capabilities to solve a performance regression.
i find it highly unlikely that nobody has raised this issue in the past. could you please perform a search of Swift Forums history to see if anyone has asked for this in the past?
You are correct that this issue has been raised before, though perhaps not as loudly or as recently as one might expect given the performance implications.
I found a direct request for this exact functionality in the "Dictionary Enhancements" mega-thread from February 2017 (which informed SE-0165).
The Historical Context
During the design of the original
mapValuesAPI, a user explicitly requested this:"One design detail. Even though it is only mapping the values, I would like it to pass the key to the closure as well. It occasionally figures in to the mapping logic."
— User discussion on Swift Forums, Feb 2017Why was it ignored?
It appears to have been a victim of API Minimalism and Syntactic Cleanness. The primary argument formapValueswas to allow a simple syntax likedict.mapValues(String.init).If the closure signature were
(Key, Value) -> T, you could no longer pass simple single-argument initializers or functions (likeInt.init) directly tomapValueswithout wrapping them in a closure like{ _, v in Int(v) }. To support both, the standard library would need an overload, which increases API surface area.Has there been a formal proposal?
No. While it was mentioned in comments, I cannot find a dedicated SE (Swift Evolution) Proposal or even a standalone "Pitch" thread specifically for a
mapValuesvariant that passes the key.Most related proposals focused on:
mapKeys(Topic 15342, which you mentioned).compactMapValues(which was added).- **
merge/uniquingKeysWith**patterns.The "Silent" Workaround
It seems most advanced users (including standard library contributors) quietly accepted that if you need the key, you fall back to
reduce(into:)or theinit(uniqueKeysWithValues:)pattern, even though—as you noted—it forces a pessimized re-hash of every single key.Conclusion
You are not retreading old ground where a proposal was "Rejected." You are identifying a gap that was noted 8 years ago but never formally pitched as a standalone feature.
This makes it a prime candidate for a modern pitch. The performance argument (avoiding hashing) is much stronger now that Swift is being used more heavily in server/performance contexts.