Dictionary and Set Improvements in Swift 4.0
In the latest release of Swift, dictionaries and sets gain a number of new methods and initializers that make common tasks easier than ever. Operations like grouping, filtering, and transforming values can now be performed in a single step, letting you write more expressive and efficient code.
This post explores these new transformations,
using some grocery data for a market as an example.
This custom GroceryItem
struct,
made up of a name and a department,
will serve as the data type:
struct GroceryItem: Hashable {
var name: String
var department: Department
enum Department {
case bakery, produce, seafood
}
static func ==(lhs: GroceryItem, rhs: GroceryItem) -> Bool {
return (lhs.name, lhs.department) == (rhs.name, rhs.department)
}
var hashValue: Int {
// Combine the hash values for the name and department
return name.hashValue << 2 | department.hashValue
}
}
// Create some groceries for our store:
let π = GroceryItem(name: "Apples", department: .produce)
let π = GroceryItem(name: "Bananas", department: .produce)
let π₯ = GroceryItem(name: "Croissants", department: .bakery)
let π = GroceryItem(name: "Salmon", department: .seafood)
let π = GroceryItem(name: "Grapes", department: .produce)
let π = GroceryItem(name: "Bread", department: .bakery)
let π€ = GroceryItem(name: "Shrimp", department: .seafood)
let groceries = [π, π, π₯, π, π, π, π€]
The examples that follow use the groceries
array
to build and transform dictionaries with these new tools.
Grouping Values by a Key
A new grouping initializer makes it a snap to build a dictionary from a sequence of values, grouped by keys computed from those values. Weβll use this new initializer to build a dictionary of groceries grouped by their department.
To do this in earlier versions of Swift, you used iteration to build up a dictionary from scratch. This required type annotations, manual iteration, and a check to see if each key already existed in the dictionary.
// Swift <= 3.1
var grouped: [GroceryItem.Department: [GroceryItem]] = [:]
for item in groceries {
if grouped[item.department] != nil {
grouped[item.department]!.append(item)
} else {
grouped[item.department] = [item]
}
}
With this update to Swift,
you can use the Dictionary(grouping:by)
initializer to create the same dictionary
with a single line of code.
Pass a closure that returns a key for each element in your array.
In the following code, the closure returns the department for each grocery item:
// Swift 4.0
let groceriesByDepartment = Dictionary(grouping: groceries,
by: { item in item.department })
// groceriesByDepartment[.bakery] == [π₯, π]
The resulting groceriesByDepartment
dictionary
has an entry for each department in the list of groceries.
The value for each key is an array of the groceries within that department,
in the same order as the original list.
Using .bakery
as a key in groceriesByDepartment
gives you the array [π₯, π]
.
Transforming a Dictionaryβs Values
You can transform the values of a dictionary,
while keeping the same keys,
by using the new mapValues(_:)
method.
This code transforms the arrays of items in groceriesByDepartment
into their counts,
creating a lookup table for the number of items in each department:
let departmentCounts = groceriesByDepartment.mapValues { items in items.count }
// departmentCounts[.bakery] == 2
Because the dictionary has all the same keys,
just with different values,
it can use the same internal layout as the original dictionary
and doesnβt need to recompute any hash values.
This makes calling mapValues(_:)
faster
than rebuilding the dictionary from scratch.
Building Dictionaries from Key-Value Pairs
You can now create dictionaries from sequences of key-value pairs using two different initializers: one for when you have unique keys, and one for when you might have keys that repeat.
If you start with a sequence of keys and a sequence of values,
you can combine them
into a single sequence of pairs
using the zip(_:_:)
function.
For example,
this code creates a sequence of tuples
with the name of a grocery item and the item itself:
let zippedNames = zip(groceries.map { $0.name }, groceries)
Each element of zippedNames
is a (String, GroceryItem)
tuple,
the first of which is ("Apples", π)
.
Because every grocery item has a unique name,
the following code successfully creates a dictionary
that uses names as keys for grocery items:
var groceriesByName = Dictionary(uniqueKeysWithValues: zippedNames)
// groceriesByName["Apples"] == π
// groceriesByName["Kumquats"] == nil
Use the Dictionary(uniqueKeysWithValues:)
initializer only when youβre sure
that your data has unique keys.
Any duplicated keys in the sequence will trigger a runtime error.
If your data has (or might have) repeated keys,
use the new merging initializer, Dictionary(_:uniquingKeysWith:)
.
This initializer takes a sequence of key-value pairs
along with a closure that is called whenever a key is repeated.
The uniquing closure takes the first and second value
that share the same key as arguments,
and can return the existing value,
the new value,
or combine them however you decide.
For example,
the following code converts an array of (String, String)
tuples
into a dictionary by using Dictionary(_:uniquingKeysWith:)
.
Note that "dog"
is the key in two of the key-value pairs.
let pairs = [("dog", "π"), ("cat", "π±"), ("dog", "πΆ"), ("bunny", "π°")]
let petmoji = Dictionary(pairs,
uniquingKeysWith: { (old, new) in new })
// petmoji["cat"] == "π±"
// petmoji["dog"] == "πΆ"
When the second key-value pair with the key "dog"
is reached,
the uniquing closure is called with the old and new values ("π"
and "πΆ"
).
Because the closure always returns its second parameter,
the result has "πΆ"
as the value for the "dog"
key.
Selecting Certain Entries
Dictionaries now have a filter(_:)
method that returns a dictionary,
not just an array of key-value pairs,
like in earlier versions of Swift.
Pass a closure that takes a key-value pair as its argument
and returns true
if that pair should be in the result.
func isOutOfStock(_ item: GroceryItem) -> Bool {
// Looks up `item` in inventory
}
let outOfStock = groceriesByName.filter { (_, item) in isOutOfStock(item) }
// outOfStock["Croissants"] == π₯
// outOfStock["Apples"] == nil
This code calls an isOutOfStock(_:)
function on each item,
keeping only the grocery items that are out of stock.
Using Default Values
Dictionaries now have a second key-based subscript that makes it easier to get and update values. The following code defines a simple shopping cart, implemented as a dictionary of items and their counts:
// Begin with a single banana
var cart = [π: 1]
Because some keys may not have corresponding values in the dictionary, when you use a key to look up a value, the result is optional.
// One banana:
cart[π] // Optional(1)
// But no shrimp:
cart[π€] // nil
Instead of using the nil coalescing operator (??
)
to turn optional values into the actual count you need,
you can now subscript a dictionary with a key and a default
parameter.
If the key is found,
its value is returned and the default is ignored.
If the key isnβt found,
the subscript returns the default value you provided.
// Still one banana:
cart[π, default: 0] // 1
// And zero shrimp:
cart[π€, default: 0] // 0
You can even modify a value through the new subscript, simplifying the code needed to add new items to the cart.
for item in [π, π, π] {
cart[item, default: 0] += 1
}
When this loop processes each banana (π
),
the current value is retrieved, incremented,
and stored back into the dictionary.
When itβs time to add the loaf of bread (π
),
the dictionary doesnβt find the key,
and instead returns the default value (0
).
After that value is incremented,
the dictionary adds the new key-value pair.
At the end of the loop, cart
is [π: 3, π: 1]
.
Merging Two Dictionaries into One
In addition to easier incremental changes, dictionaries now make it simpler to make changes in bulk, with methods that merge one dictionary into another.
To merge the contents of cart
and another dictionary,
you can use the mutating merge(_:uniquingKeysWith:)
method.
The uniquing closure that you pass works the same way
as in the Dictionary(_:uniquingKeysWith:)
initializer:
Itβs called whenever there are two values with the same key,
and returns one, the other, or a combination of the values.
In this example,
passing the addition operator as the uniquingKeysWith
parameter
adds together any counts for matching keys,
so the updated cart has the correct total for each item:
let otherCart = [π: 2, π: 3]
cart.merge(otherCart, uniquingKeysWith: +)
// cart == [π: 5, π: 3, π: 1]
To create a new dictionary
with the merged contents instead of merging in place,
use the nonmutating merging(_:uniquingKeysWith:)
method.
And Thatβs Not Allβ¦
There are a few more additions we havenβt covered.
Dictionaries now have custom keys
and values
collections with new capabilities.
The keys
collection maintains fast key lookup,
while the mutable values
collection lets you modify values in place.
Like dictionaries,
sets gain a new filter(_:)
method that returns a set of the same type,
instead of an array like in earlier versions of Swift.
And finally,
both sets and dictionaries now expose their current capacity
and add a reserveCapacity(_:)
method.
With these additions you can see and control the size of their internal storage.
Other than the custom keys
and values
collections,
all these changes are available in Swift 3.2.
Even if you havenβt yet switched to using Swift 4.0,
you can start taking advantage of these improvements today!
You can find more information about all these new capabilities
in the Dictionary and Set documentation,
or read more about the rationale behind the additions
in the Swift Evolution proposals for the
custom keys
and values
collections and
other dictionary and set enhancements.