Giving Dictionary.mapValues(_:) access to the associated key

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 Dictionary internally 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" mapValuesWithKey in user-space Swift, we have to look at how mapValues works internally versus the public initializers.

  1. 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.
  1. 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:

  1. API Minimalism: The original functional programming patterns for "Functors" usually imply that map transforms the content (value) without regard to the context (index/key).
  2. Oversight: mapValues was 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 NewValue is the same type as OldValue, 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 new stride (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 mapValues API, 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 2017

Why was it ignored?
It appears to have been a victim of API Minimalism and Syntactic Cleanness. The primary argument for mapValues was to allow a simple syntax like dict.mapValues(String.init).

If the closure signature were (Key, Value) -> T, you could no longer pass simple single-argument initializers or functions (like Int.init) directly to mapValues without 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 mapValues variant that passes the key.

Most related proposals focused on:

  1. mapKeys (Topic 15342, which you mentioned).
  2. compactMapValues (which was added).
  3. **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 the init(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.

8 Likes

The lazy isn't even helpful for throwing transforms. Many operations need a corresponding overhaul. I don't think this one should be singled out, but it is a good example of the general problem.

@inlinable func mapToValues<Transformed, Error>(
  _ transform: (Element) throws(Error) -> Transformed
) throws(Error) -> [Key: Transformed] {
  .init(uniqueKeysWithValues: zip(keys, try map(transform)))    
}

Here’s an implementation relying on the stable-but-unspecified[1] iteration order of Dictionary:

extension Dictionary {
  func mapElements<R>(
    _ transform: (Element) throws -> R
  ) rethrows -> [Key: R] {
    var iterator = makeIterator()
    return try mapValues { _ in
      try transform(iterator.next()!)
    }
  }
}

let dict = [1: "one", 2: "two", 3: "three"]
print(dict.mapElements { "\($0)-\($1)" })
//=> [2: "2-two", 1: "1-one", 3: "3-three"]

(I must add that it feels a bit dirty to rely on stdlib implementation details like that. :laughing:)


  1. All iterations through the original dictionary or its keys or its values or its indicestraverse the elements in the same order, whatever it happens to be. ↩︎

2 Likes

I have run into this a couple times too and the performance I leave on the table always makes me sad. I have called this method mapValuesWithKeys before but I don’t quite love it yet. It would also make sense to add this to OrderedDictionary from swift-collections.

3 Likes

In case anybody else is interested about “the historical context” from the LLM opinion, this is the original comment: Dictionary Enhancements - #4 by Jon_Hull