The surprising awesomeness of Grouped Dictionaries

Yesterday, I was chatting about ways to partition a stream of values. I wanted to collect values into new streams: values that satisfied a predicate, and those that did not. A number of hugely complicated approaches were discussed until Nate Cook brought up a fantastic new Swift 4.0 API. The Dictionary type’s init(grouping:by:) call allows you to convert any sequence to a dictionary by grouping its elements.

Pass the initializer a sequence and a closure, and the initializer creates entries for each value returned by the closure. For a predicate, you end up with two groups: one populated for true predicate values, one for false:

let numbers = 1 ... 20
let predicate: (Int) -> Bool = {  $0 % 2 == 0 }
let grouping = Dictionary(grouping: numbers, by: predicate)
print(grouping)
// [false: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], 
//  true: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]]

Once partitioned, you can pull values from each collection (true and false) and operate on the members of that particular group:

// Iterate through the even members of this sequence
for number in (grouping[true, default:[]]) { ... }

This example uses Swift’s new default value in its grouping look-up:

grouping[true, default:[]]

If you haven’t started using this new feature, you should really adopt it into your work flow. It’s wonderful. With this call, a dictionary returns the default value when a key is not found. This avoids forced unwraps (dictionary lookups normally return optionals) and acts as an alias for nil coalescing. In this example, the default call is an alias for (grouping[true] ?? []).

Dictionary grouping provides a solid solution for sequence partitioning by predicates. But you can also do a lot more with this API. Let me give you a bunch of examples that showcase the power of this one little call.

This example creates a dictionary of names grouped by first letter. Swift creates an entry for each unique capitalized letter it finds within the name collection:

let names = ["bob", "Amelia", "joe", "alice", "jane"].map({ $0.capitalized })
let nameDict = Dictionary(grouping: names.filter({ !$0.isEmpty })) {
    $0.prefix(1)
}
print(nameDict)
// ["J": ["Joe", "Jane"], "B": ["Bob"], "A": ["Amelia", "Alice"]]

You could easily expand this example to disregard diacritical marks by stripping them through a StringTransform. (This approach is left as an exercise for the reader. 😀 )

In more realistic text-based grouping, the information you want to group on is often a level or two down within a structure. Swift keypaths make it easy to access the information you need for grouping. This next example constructs a keypath to a contact’s last name, and uses that keypath to provide the partition keys for the dictionary.

/// A person
struct Person {
    let firstName: String
    let lastName: String
}

/// A contact entry
struct Contact: CustomStringConvertible {
    let name: Person
    let address: String
    var description: String { return "\(name.firstName) \(name.lastName) at \(address)" }
}

/// Construct some contacts
let lasts = ["smith", "jones", "simpson", "cheese", "putty"]
let contacts: [Contact] = zip(names, lasts)
    .map({ Person(firstName: $0.0.capitalized, lastName: $0.1.capitalized) })
    .map({ Contact(name: $0, address: "1 Main St" )})

// Establish keypath to the last name field
let lastNameKeypath = \Contact.name.lastName

// Construct the address book based on the first letter
// of the contact's last name, then print the
// address book
let addressBook = Dictionary(grouping: contacts) { 
    $0[keyPath: lastNameKeypath].prefix(1).uppercased() 
}
for (key, value) in addressBook { 
    print(key, value) 
}

// J [Amelia Jones at 1 Main St]
// C [Alice Cheese at 1 Main St]
// P [Jane Putty at 1 Main St]
// S [Bob Smith at 1 Main St, Joe Simpson at 1 Main St]

Grouped dictionaries aren’t limited to strings and numbers. They can be quite helpful when working with interface elements. For example, in a complex user-managed  presentation, you might group button views based on their control state:

// buttonArray is [NSButton]
let buttons = Dictionary(grouping: buttonArray, by: { $0.state })

Although all the examples so far have used sequence data to produce the keys used in grouping, those keys needn’t be pulled directly from the values they categorize. Enumerations make a great Swift choice for categorizing data. This example revisits the even/odd grouping shown in the first example of this write-up but replaces the true/false predicate values with a Parity enumeration.

enum Parity {
    case even, odd
    init(_ value: Int) {
        self = value % 2 == 0 ? .even : .odd
    }
}
let parity = Dictionary(grouping: 0 ..< 10 , by: Parity.init )

You can use this same approach with more extensive enumerations and more complicated data. That said, you can take exactly this example, and simplify it enormously by grouping numbers based on a simple function. For example, you can use the direct results of the modulo operator (which returns 1 and 0)  as the keys to your grouped dictionary:

let parity2 = Dictionary(grouping: 0 ..< 10) { $0 % 2 }

In the end, it’s up to you on how you want to split your sequence, and the keys you want to represent the subsequences derived from that split. Hopefully this post has given you a few ideas to inspire your own partitioning schemes.

If you like my write-ups, please consider buying a book. Thanks to everyone who contributed to the discussion about partitioning sequences, especially Nate Cook, Tim Vermeulen, Kabir Oberai, Daniel Jalkut, Tom Harrington, Soroush Khanlou, Paul Cantrell, and Zachary Drayer.

2 Comments