Swift Generics, Constraints, and KeyPaths
The problem here is that SortDescriptor
is generic on both T
and V
, but you only want it to be generic on T
. That is, you want a SortDescriptor<Person>
, because you care that it compares Person
. You don't want a SortDescriptor<Person, String>
, because once it's created, you don't care that it's comparing on some String
property of Person
.
Probably the easiest way to “hide” the V
is by using a closure to wrap the key path:
struct SortDescriptor<T> {
var ascending: Bool
var primitiveCompare: (T, T) -> Bool
init<V: Comparable>(keyPath: KeyPath<T, V>, ascending: Bool = true) {
primitiveCompare = { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
self.ascending = ascending
}
func compare(_ a: T, _ b: T) -> Bool {
return ascending ? primitiveCompare(a, b) : primitiveCompare(b, a)
}
}
var descriptors = [SortDescriptor(keyPath: \Person.name), SortDescriptor(keyPath: \.age)]
// Inferred type: [SortDescriptor<Person>]
After that, you may want a convenient way to use a sequence of SortDescriptor
to compare to objects. For that, we'll need a protocol:
protocol Comparer {
associatedtype Compared
func compare(_ a: Compared, _ b: Compared) -> Bool
}
extension SortDescriptor: Comparer { }
And then we can extend Sequence
with a compare
method:
extension Sequence where Element: Comparer {
func compare(_ a: Element.Compared, _ b: Element.Compared) -> Bool {
for comparer in self {
if comparer.compare(a, b) { return true }
if comparer.compare(b, a) { return false }
}
return false
}
}
descriptors.compare(jim, bob)
// false
If you're using a newer version of Swift with conditional conformances, you should be able to conditionally conform Sequence
to Comparer
by changing the first line of the extension to this:
extension Sequence: Comparer where Element: Comparer {
Expanding on @Rob Mayoff's answer, here's a full sorting solution
enum SortDescriptorComparison {
case equal
case greaterThan
case lessThan
}
struct SortDescriptor<T> {
private let compare: (T, T) -> SortDescriptorComparison
let ascending : Bool
init<V: Comparable>(_ keyPath: KeyPath<T,V>, ascending:Bool = true) {
self.compare = {
let v1 = $0[keyPath: keyPath]
let v2 = $1[keyPath: keyPath]
if v1 == v2 {
return .equal
} else if v1 > v2 {
return .greaterThan
} else {
return .lessThan
}
}
self.ascending = ascending
}
func compare(v1:T, v2:T) -> SortDescriptorComparison {
return compare(v1, v2)
}
}
extension Array {
mutating func sort(sortDescriptor: SortDescriptor<Element>) {
self.sort(sortDescriptors: [sortDescriptor])
}
mutating func sort(sortDescriptors: [SortDescriptor<Element>]) {
self.sort() {
for sortDescriptor in sortDescriptors {
switch sortDescriptor.compare(v1: $0, v2: $1) {
case .equal:
break
case .greaterThan:
return !sortDescriptor.ascending
case .lessThan:
return sortDescriptor.ascending
}
}
return false
}
}
}
extension Sequence {
func sorted(sortDescriptor: SortDescriptor<Element>) -> [Element] {
return self.sorted(sortDescriptors: [sortDescriptor])
}
func sorted(sortDescriptors: [SortDescriptor<Element>]) -> [Element] {
return self.sorted() {
for sortDescriptor in sortDescriptors {
switch sortDescriptor.compare(v1: $0, v2: $1) {
case .equal:
break
case .greaterThan:
return !sortDescriptor.ascending
case .lessThan:
return sortDescriptor.ascending
}
}
return false
}
}
}
struct Person {
let name : String
let age : Int
}
let jim = Person(name: "Jim", age: 25)
let bob = Person(name: "Bob", age: 30)
let tim = Person(name: "Tim", age: 25)
let abe = Person(name: "Abe", age: 20)
let people = [tim, jim, bob, abe]
let sorted = people.sorted(sortDescriptors: [SortDescriptor(\Person.age), SortDescriptor(\Person.name)])
print(sorted) //Abe, Jim, Time, Bob