I'm aware of the limitations of generics in Swift and why they exist so this is not a question about compiler errors. Rather, I occasionally run into situations that seem as though they should be possible with some combination of the resources available in (i.e. generics, associatedTypes/protocols, etc) but can't seem to work out a solution.
In this example, I'm trying to come up with a Swift replacement for NSSortDescriptor (just for fun). It works perfect when you only have one descriptor but, as is often done with the NS version, it would be nice to create an array of SortDescriptors to sort on multiple keys.
The other trial here is using Swift KeyPaths. Because those require a Value type and the comparison requires a Comparable value, I'm running into trouble figuring out where/how to define the types to satisfy everything.
Is this possible? Here is one of the closest solutions I've come up with, but, as you can see at the bottom, it falls short when building an array.
Again, I understand why this doesn't work as is, but am curious if there is a way to achieve the desired functionality.
struct Person {
let name : String
let age : Int
}
struct SortDescriptor<T, V:Comparable> {
let keyPath: KeyPath<T,V>
let ascending : Bool
init(_ keyPath: KeyPath<T,V>, ascending:Bool = true) {
self.keyPath = keyPath
self.ascending = ascending
}
func compare(obj:T, other:T) -> Bool {
let v1 = obj[keyPath: keyPath]
let v2 = other[keyPath: keyPath]
return ascending ? v1 < v2 : v2 < v1
}
}
let jim = Person(name: "Jim", age: 30)
let bob = Person(name: "Bob", age: 35)
let older = SortDescriptor(\Person.age).compare(obj: jim, other: bob) // true
// Heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional
var descriptors = [SortDescriptor(\Person.age), SortDescriptor(\Person.name)]
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 {