KeyValuePairs
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
Trigram | ☰ | ☱ | ☲ | ☳ | ☴ | ☵ | ☶ | ☷ |
---|---|---|---|---|---|---|---|---|
Nature |
|
|
|
|
|
|
|
|
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
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
Option
(as explained in a previous article),
but we’d be remiss to try and shoehorn Range
and Closed
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 Key
,
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.
Key
is a structure in the Swift standard library that —
surprise, surprise —
represents a collection of key-value pairs.
struct Key Value Pairs<Key, Value>: Expressible By Dictionary Literal,
Random Access Collection
{
typealias Element = (key: Key, value: Value)
typealias Index = Int
typealias Indices = Range<Int>
typealias Sub Sequence = Slice<Key Value Pairs>
…
}
This truncated declaration highlights the defining features of Key
:
- 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 Expressible
protocol’s required initializer:
protocol Expressible By Dictionary Literal {
associatedtype Key
associatedtype Value
init(dictionary Literal elements: (Key, Value)...)
}
This confusion was amplified by the existence of a Dictionary
type,
which was only recently renamed to Key
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 Key
object
with a dictionary literal
(in fact, this is the only way to create one):
let pairs: Key Value Pairs<String, String> = [
"木": "wood",
"火": "fire",
"土": "ground",
"金": "metal",
"水": "water"
]
KeyValuePairs as Random-Access Collection
Key
conforms to Random
,
which allows its contents to be retrieved by (in this case, Int
) indices.
In contrast to Array
,
Key
doesn’t conform to Range
,
so you can’t append elements or remove individual elements at indices or ranges.
This narrowly constrains Key
,
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,
Key
are found in just two places:
struct Mirror {
init<Subject>(_ subject: Subject,
children: Key Value Pairs<String, Any>,
display Style: Display Style? = nil,
ancestor Representation: Ancestor Representation = .generated)
}
typealias RGBA = UInt32
typealias RGBAComponents = (UInt8, UInt8, UInt8, UInt8)
let color: RGBA = 0x FFEFD5FF
let mirror = Mirror(color,
children: ["name": "Papaya Whip",
"components": (0x FF, 0x EF, 0x D5, 0x FF) as RGBAComponents],
display Style: .struct)
mirror.children.first(where: { (label, _) in label == "name" })?.value
// "Papaya Whip"
@dynamic Callable
struct Keyword Callable {
func dynamically Call(with Keyword Arguments args: Key Value Pairs<String, Int>) -> Int {
return args.count
}
}
let object = Keyword Callable()
object(a: 1, 2) // desugars to `object.dynamically Call(with Keyword Arguments: ["a": 1, "": 2])`
On both occasions,
Key
is employed as an alternative to [(Key, Value)]
to enforce restraint by the caller.
Without any other public initializers,
Key
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 Key
,
you’ll first want to convert it into a conventional Collection
type —
either Array
or Dictionary
.
Converting to Arrays
Key
is a Sequence
,
by virtue of its conformance to Random
(and therefore Collection
).
When we pass it to the corresponding Array
initializer,
it becomes an array of its associated Element
type ((Key, Value)
).
let array Of Pairs: [(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 Key
object,
but creating a Dictionary
is more complicated.
Converting to Dictionaries
There are four built-in types that conform to Expressible
:
Dictionary
NSDictionary
NSMutable
Dictionary Key
Value Pairs
Each of the three dictionary types constitutes a
surjective mapping,
such that every value element has one or more corresponding keys.
Key
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(unique
,
init(_:uniquing
, and
init(grouping:by)
Consider the following example that
constructs a Key
object with a duplicate key:
let pairs With Duplicate Key: Key Value Pairs<String, String> = [
"天": "Heaven",
"澤": "Lake",
"澤": "Marsh",
…
]
Attempting to pass this to init(unique
results in a fatal error:
Dictionary<String, Int>(unique Keys With Values: Array(pairs With Duplicate Key))
// 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(pairs With Duplicate Key),
uniquing Keys With: { (first, _) in first })
// ["澤": "Lake", …]
Dictionary(Array(pairs With Duplicate Key),
uniquing Keys With: { (_, last) in last })
// ["澤": "Marsh", …]
Dictionary(grouping: Array(pairs With Duplicate Key),
by: { (pair) in pair.value })
// ["澤": ["Lake", "Marsh"], …]
Outside of its narrow application in the standard library,
Key
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,
Key
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.