Alejandro Martinez

BlogTagsPodcastPortfolio Youtube

24 August, 2018SwiftAlgorithms and Data Structures🕐 3min read

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.

groupBy in Swift 4

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.

👉🏻 If you have any feedback you can reach me at alexito4

Swift 4.2 CaseIterable enums and UISegmentedControl, a practical example

Alejandro Martinez
alexito4@gmail.com

Buka pintu

alexito4
alexito4