GroupBy with KeyPath in Swift 4
One of the best things of modern Swift is the introduction of KeyPath
. It allows us to reference getters and setters as first class values in our code. With them you can simplify a bunch of code and make it less error prone. In this post I’m gonna evolve the groupBy
function from GroupBy in Swift 2.0 to use this new functionality.
First of all let’s recap what the functionality of groupBy
is.
let people = [
Person(name: "Alex", priority: 1),
Person(name: "Anna", priority: 1),
Person(name: "Julian", priority: 1)
]
people.groupBy({ (l: Person, r: Person) -> Bool in
return l.name.first == r.name.first
})
// Returns:
[
[
Person(name: "Alex", priority: 1),
Person(name: "Anna", priority: 1)
],
[
Person(name: "Julian", priority: 1)
]
]
As you can see the groupBy
function takes an array and groups it’s elements in nested arrays based on a condition given by a closure that compares two instances.
Thanks to Swift extensions we can add this functionality to the Collection protocol itself.
extension Collection {
public func groupBy(_ grouper: (Element, Element) -> Bool) -> [[Element]] {
...
}
}
The updated source code compatible with Swift 4 is available in GitHub.
Sidenote: grouping with Dictionary
Dictionary now has a generic initialiser init(grouping:by:)
that allows us to group the elements of a Sequence.
Dictionary(grouping: people, by: { return $0.name.first })
This initialiser creates a Dictionary where each group is represented by a key and the values of that key are an array of all the elements on that group.
This is an interesting addition but the inconvenience is that we lose the original order of the array.
KeyPath to improve APIs
The original implementation of groupBy
has the inconvenience that the given closure accepts two instances of the same object and you need to compare them.
{ (l: Person, r: Person) -> Bool in
return l.name.first == r.name.first
}
This is the most flexible API possible as you give all the information to the caller to allow the best decision to be taken. But it’s also really error prone. Everytime you see code like this be careful, there is a high chance that you are comparing the same instance!
Here is where KeyPath
can be of a great help. Similar to the implementation of the Dictionary initialiser, the goal is to just have to give the property that we want to group by, so we can’t make the error of comparing the wrong instances or values.
The Dictionary initialiser shows us a way, by returning in the closure the Key
that needs to be used for grouping. This is also a flexible solution as it allows the caller to return an arbitrary key that it’s not even part of the elements themselves.
But for my use case I prefer the simplicity of KeyPath. I can give the groupBy
function a reference to the property that I want to be grouped by. And because KeyPath
can drill down the properties of an object we can easily get the first character of a String.
people.groupBy(\Person.name.first)
This call returns the same as the original one with the closure but it’s much shorter and concise. It really expresses the intention of “give me people grouped by first letter of their name”, without the fuss of closure syntax and comparisons.
Remember that you can find the source code in available in GitHub.