Key​Value​Pairs

Cosmologies seek to create order by dividing existence into discrete, interdependent parts. Thinkers in every society throughout history have posited various arrangements — though Natural numbers being what they are, there are only so many ways to slice the ontological pie.

There are dichotomies like 陰陽 ( yīnyáng ) ( ) : incontrovertible and self-evident (albeit reductive). There are trinities, which position man in relation to heaven and earth. One might divide everything into four, like the ancient Greeks with the elements of earth, water, air, and fire. Or you could carve things into five, like the Chinese with Wood ( ) , Fire ( huǒ ) , Earth ( ) , Metal ( jīn ) , and Water ( shuǐ ) . Still not satisfied? Perhaps the eight-part 八卦 (bāguà) will provide the answers that you seek:

Trigram
Nature ( Heaven ) ( Lake / Marsh ) ( Fire ) ( Thunder ) ( Wind ) ( Water ) ( Mountain ) ( Ground )

Despite whatever galaxy brain opinion we may have about computer science, the pragmatic philosophy of day-to-day programming more closely aligns with a mundane cosmology; less imago universi, more じゃんけん jan-ken ( Rock-Paper-Scissors ✊🤚✌️ ) .

For a moment, ponder the mystical truths of fundamental Swift collection types:

Arrays are ordered collections of values.
Sets are unordered collections of unique values.
Dictionaries are unordered collections of key-value associations. The Book of Swift

Thus compared to the pantheon of java.util collections or std containers, Swift offers a coherent coalition of three. Yet, just as we no longer explain everyday phenomena strictly in terms of humors or æther, we must reject this formulation. Such a model is incomplete.

We could stretch our understanding of sets to incorporate OptionSet (as explained in a previous article), but we’d be remiss to try and shoehorn Range and ClosedRange into the same bucket as Array — and that’s to say nothing of the panoply of Swift Collection Protocols (an article in dire need of revision).

This week on NSHipster, we’ll take a look at KeyValuePairs, a small collection type that challenges our fundamental distinctions between Array, Set, and Dictionary. In the process, we’ll gain a new appreciation and a deeper understanding of the way things work in Swift.


KeyValuePairs is a structure in the Swift standard library that — surprise, surprise — represents a collection of key-value pairs.

struct KeyValuePairs<Key, Value>: ExpressibleByDictionaryLiteral,
                                  RandomAccessCollection
{
  typealias Element = (key: Key, value: Value)
  typealias Index = Int
  typealias Indices = Range<Int>
  typealias SubSequence = Slice<KeyValuePairs>

  
}

This truncated declaration highlights the defining features of KeyValuePairs:

  • Its ability to be expressed by a dictionary literal
  • Its capabilities as a random-access collection

KeyValuePairs as Expressible by Dictionary Literal

Literals allow us to represent values directly in source code, and Swift is rather unique among other languages by extending this functionality to our own custom types through protocols.

A dictionary literal represents a value as mapping of keys and values like so:

["key": "value"]

However, the term “dictionary literal” is a slight misnomer, since a sequence of key-value pairs — not a Dictionary — are passed to the ExpressibleByDictionaryLiteral protocol’s required initializer:

protocol ExpressibleByDictionaryLiteral {
    associatedtype Key
    associatedtype Value

    init(dictionaryLiteral elements: (Key, Value)...)
}

This confusion was amplified by the existence of a DictionaryLiteral type, which was only recently renamed to KeyValuePairs in Swift 5. The name change served to both clarify its true nature and bolster use as a public API (and not some internal language construct).

You can create a KeyValuePairs object with a dictionary literal (in fact, this is the only way to create one):

let pairs: KeyValuePairs<String, String> = [
    "木": "wood",
    "火": "fire",
    "土": "ground",
    "金": "metal",
    "水": "water"
]

KeyValuePairs as Random-Access Collection

KeyValuePairs conforms to RandomAccessCollection, which allows its contents to be retrieved by (in this case, Int) indices. In contrast to Array, KeyValuePairs doesn’t conform to RangeReplaceableCollection, so you can’t append elements or remove individual elements at indices or ranges. This narrowly constrains KeyValuePairs, such that it’s effectively immutable once initialized from a dictionary literal.

These functional limitations are the key to understanding its narrow application in the standard library.

KeyValuePairs in the Wild

Across the Swift standard library and Apple SDK, KeyValuePairs are found in just two places:

struct Mirror {
    init<Subject>(_ subject: Subject,
                  children: KeyValuePairs<String, Any>,
                  displayStyle: DisplayStyle? = nil,
                  ancestorRepresentation: AncestorRepresentation = .generated)
}

typealias RGBA = UInt32
typealias RGBAComponents = (UInt8, UInt8, UInt8, UInt8)

let color: RGBA = 0xFFEFD5FF
let mirror = Mirror(color,
                    children: ["name": "Papaya Whip",
                               "components": (0xFF, 0xEF, 0xD5, 0xFF) as RGBAComponents],
                    displayStyle: .struct)

mirror.children.first(where: { (label, _) in label == "name" })?.value
// "Papaya Whip"
@dynamicCallable
struct KeywordCallable {
  func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Int {
    return args.count
  }
}

let object = KeywordCallable()
object(a: 1, 2) // desugars to `object.dynamicallyCall(withKeywordArguments: ["a": 1, "": 2])`

On both occasions, KeyValuePairs is employed as an alternative to [(Key, Value)] to enforce restraint by the caller. Without any other public initializers, KeyValuePairs can only be constructed from dictionary literals, and can’t be constructed dynamically.

Working with KeyValuePairs Values

If you want to do any kind of work with a KeyValuePairs, you’ll first want to convert it into a conventional Collection type — either Array or Dictionary.

Converting to Arrays

KeyValuePairs is a Sequence, by virtue of its conformance to RandomAccessCollection (and therefore Collection). When we pass it to the corresponding Array initializer, it becomes an array of its associated Element type ((Key, Value)).

let arrayOfPairs: [(Key, Value)] = Array(pairs)

Though, if you just want to iterate over each key-value pair, its conformance to Sequence means that you can pass it directly to a for-in loop:

for (key, value) in pairs {
    
}

You can always create an Array from a KeyValuePairs object, but creating a Dictionary is more complicated.

Converting to Dictionaries

There are four built-in types that conform to ExpressibleByDictionaryLiteral:

  • Dictionary
  • NSDictionary
  • NSMutableDictionary
  • KeyValuePairs

Each of the three dictionary types constitutes a surjective mapping, such that every value element has one or more corresponding keys. KeyValuePairs is the odd one out: it instead maintains an ordered list of tuples that allows for duplicate key associations.

Dictionary got a number of convenient initializers in Swift 4 thanks to SE-0165 (thanks, Nate!), including init(uniqueKeysWithValues:), init(_:uniquingKeysWith:), and init(grouping:by)

Consider the following example that constructs a KeyValuePairs object with a duplicate key:

let pairsWithDuplicateKey: KeyValuePairs<String, String> = [
    "天": "Heaven",
    "澤": "Lake",
    "澤": "Marsh",
    
]

Attempting to pass this to init(uniqueKeysWithValues:) results in a fatal error:

Dictionary<String, Int>(uniqueKeysWithValues: Array(pairsWithDuplicateKey))
// Fatal error: Duplicate values for key: '澤'

Instead, you must either specify which value to map or map each key to an array of values:

Dictionary(Array(pairsWithDuplicateKey),
                 uniquingKeysWith: { (first, _) in first })
// ["澤": "Lake", ]

Dictionary(Array(pairsWithDuplicateKey),
                 uniquingKeysWith: { (_, last) in last })
// ["澤": "Marsh", ]

Dictionary(grouping: Array(pairsWithDuplicateKey),
           by: { (pair) in pair.value })
// ["澤": ["Lake", "Marsh"], ]

Outside of its narrow application in the standard library, KeyValuePairs are unlikely to make an appearance in your own codebase. You’re almost always better off going with a simple [(Key, Value)] tuple array.

Much as today’s Standard Model more closely resembles the cacophony of a zoo than the musica universalis of celestial spheres, KeyValuePairs challenges our tripartite view of Swift collection types. But like all cosmological exceptions — though uncomfortable or even unwelcome at times — it serves to expand our understanding.

That’s indeed its key value.

NSMutableHipster

Questions? Corrections? Issues and pull requests are always welcome.

This article uses Swift version 5.1. Find status information for all articles on the status page.

Written by Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon.

Next Article

The process of booting a computer is a small miracle. Starting with a blank slate, a computer incrementally runs smaller, simpler programs to load larger, more capable programs into memory, until it finally has everything it needs to run the operating system itself.