It seems like it should be possible to use an array of KeyPath
s as sort keys to sort an array of Swift structs using an arbitrary number of sort keys.
Conceptually, it's simple. You define an array of KeyPaths to a generic object, where the only restriction is that the properties in the keypath be Comparable
.
As long as all the KeyPaths point to properties of the same type, all is well. However, as soon as you try to use KeyPaths that point to elements of different types into the array, it stops working.
See the code below. I create a simple struct with 2 Int properties and a double property. I create an extension to Array that implements a function sortedByKeypaths(_:)
That function specifies a generic type PROPERTY that is Comparable. It takes an array of kepaths to some object Element that specifies properties of type PROPERTY. (Comparable properties.)
As long as you call that function using an array of KeyPaths to properties that are all of the same type, it works perfectly.
However, if you try to pass in array of keypaths to properties of different types, it throws an error "cannot convert value of type '[PartialKeyPath]' to expected argument type '[KeyPath]'"
Because the array contains heterogenious keypaths, the array devolves to type '[PartialKeyPath]` due to type erasure, and you can't use a PartialKeyPath to fetch elements from an array.
Is there a solution to this problem? Not being able to use a heterogenious array of KeyPaths seems like it severely limits the usefulness of Swift KeyPaths
import UIKit
struct Stuff {
let value: Int
let value2: Int
let doubleValue: Double
}
extension Array {
func sortedByKeypaths<PROPERTY: Comparable>(_ keypaths: [KeyPath<Element, PROPERTY>]) -> [Element] {
return self.sorted { lhs, rhs in
var keypaths = keypaths
while !keypaths.isEmpty {
let keypath = keypaths.removeFirst()
if lhs[keyPath: keypath] != rhs[keyPath: keypath] {
return lhs[keyPath: keypath] < rhs[keyPath: keypath]
}
}
return true
}
}
}
var stuff = [Stuff]()
for _ in 1...20 {
stuff.append(Stuff(value: Int(arc4random_uniform(5)),
value2: Int(arc4random_uniform(5)),
doubleValue: Double(arc4random_uniform(10))))
}
let sortedStuff = stuff.sortedByKeypaths([\Stuff.value, \Stuff.value2]) //This works
sortedStuff.forEach { print($0) }
let moreSortedStuff = stuff.sortedByKeypaths([\Stuff.value, \Stuff.doubleValue]) //This throws a compiler error
moreSortedStuff.forEach { print($0) }
The problem with using an array of partial keypaths is that you have no guarantee that the property types are Comparable
. One possible solution would be to use a type erasing wrapper in order to erase the value type of a key path, while ensuring that it is Comparable
:
struct PartialComparableKeyPath<Root> {
private let _isEqual: (Root, Root) -> Bool
private let _isLessThan: (Root, Root) -> Bool
init<Value : Comparable>(_ keyPath: KeyPath<Root, Value>) {
self._isEqual = { $0[keyPath: keyPath] == $1[keyPath: keyPath] }
self._isLessThan = { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
}
func isEqual(_ lhs: Root, _ rhs: Root) -> Bool {
return _isEqual(lhs, rhs)
}
func isLessThan(_ lhs: Root, _ rhs: Root) -> Bool {
return _isLessThan(lhs, rhs)
}
}
Then you could implement your sorting function as:
extension Sequence {
func sorted(by keyPaths: PartialComparableKeyPath<Element>...) -> [Element] {
return sorted { lhs, rhs in
for keyPath in keyPaths {
if !keyPath.isEqual(lhs, rhs) {
return keyPath.isLessThan(lhs, rhs)
}
}
return false
}
}
}
and then use like this:
struct Stuff {
let value: Int
let value2: Int
let doubleValue: Double
}
var stuff = [Stuff]()
for _ in 1 ... 20 {
stuff.append(Stuff(value: Int(arc4random_uniform(5)),
value2: Int(arc4random_uniform(5)),
doubleValue: Double(arc4random_uniform(10))))
}
let sortedStuff = stuff.sorted(by: PartialComparableKeyPath(\.value),
PartialComparableKeyPath(\.value2))
sortedStuff.forEach { print($0) }
let moreSortedStuff = stuff.sorted(by: PartialComparableKeyPath(\.value),
PartialComparableKeyPath(\.doubleValue))
moreSortedStuff.forEach { print($0) }
Though unfortunately, this requires wrapping each individual keypath in a PartialComparableKeyPath
value in order to capture and erase the value type for the keypath, which isn't particularly pretty.
Really the feature we need here is variadic generics, which would let you define your function over a variable number of generic placeholders for the value types of your keypaths, each constrained to Comparable
.
Until then, another option would be to just write a given number of overloads for the different number of keypaths to compare:
extension Sequence {
func sorted<A : Comparable>(by keyPathA: KeyPath<Element, A>) -> [Element] {
return sorted { lhs, rhs in
lhs[keyPath: keyPathA] < rhs[keyPath: keyPathA]
}
}
func sorted<A : Comparable, B : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable, D : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>, _ keyPathD: KeyPath<Element, D>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC], lhs[keyPath: keyPathD]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC], rhs[keyPath: keyPathD])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>, _ keyPathD: KeyPath<Element, D>, _ keyPathE: KeyPath<Element, E>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC], lhs[keyPath: keyPathD], lhs[keyPath: keyPathE]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC], rhs[keyPath: keyPathD], rhs[keyPath: keyPathE])
}
}
func sorted<A : Comparable, B : Comparable, C : Comparable, D : Comparable, E : Comparable, F : Comparable>
(by keyPathA: KeyPath<Element, A>, _ keyPathB: KeyPath<Element, B>, _ keyPathC: KeyPath<Element, C>, _ keyPathD: KeyPath<Element, D>, _ keyPathE: KeyPath<Element, E>, _ keyPathF: KeyPath<Element, F>) -> [Element] {
return sorted { lhs, rhs in
(lhs[keyPath: keyPathA], lhs[keyPath: keyPathB], lhs[keyPath: keyPathC], lhs[keyPath: keyPathD], lhs[keyPath: keyPathE], lhs[keyPath: keyPathF]) <
(rhs[keyPath: keyPathA], rhs[keyPath: keyPathB], rhs[keyPath: keyPathC], rhs[keyPath: keyPathD], rhs[keyPath: keyPathE], rhs[keyPath: keyPathF])
}
}
}
I've defined them up to 6 keypaths, which should be sufficient for the majority of sorting cases. We're taking advantage of the lexicographic tuple comparison overloads of <
here, as also demonstrated here.
While the implementation isn't pretty, the call-site now looks much better, as it lets you say:
let sortedStuff = stuff.sorted(by: \.value, \.value2)
sortedStuff.forEach { print($0) }
let moreSortedStuff = stuff.sorted(by: \.value, \.doubleValue)
moreSortedStuff.forEach { print($0) }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With