Hey everybody β I'd like to pitch and socialize a new method for sorting Collections using a KeyPath.
Today, to sort a Collection of non-Comparable elements, we write a closure that returns whether or not two elements areInIncreasingOrder:
[[1, 2, 3], [1], [1, 2]].sorted(by: { $0.count < $1.count })
// returns [[1], [1, 2], [1, 2, 3]]
This has a few features that you could consider "pain points":
- The call site doesn't form a grammatical english phrase. [1]
- It's relatively easy to mix up <and>, and it isn't immediately obvious from the call site which operator results in which ordering.
- Repeating the property for both $0and$1is a touch verbose.
Key Paths could come in handy here. If we had a sort variant that accepted a KeyPath, we could instead write:
[[1, 2, 3], [1], [1, 2]].sorted(by: \.count)
// returns [[1], [1, 2], [1, 2, 3]]
This is a big improvement in terms of expressivity, clarity, and brevity!
A less-arbitrary example could look something like this:
struct OS: Equatable, Hashable {
    let name: String
    let yearReleased: Int
    
    static let supported = Set([
        OS(name: "FreeBSD", yearReleased: 1993),
        OS(name: "iOS", yearReleased: 2007),
        OS(name: "macOS", yearReleased: 2001), ...])
}
OS.supported.sorted(by: \.name)
OS.supported.sorted(by: \.yearReleased)
// instead of
OS.supported.sorted(by: { $0.name < $1.name })
OS.supported.sorted(by: { $0.yearReleased < $1.yearReleased })
Implementation
Here's one potential implementation:
extension Sequence {
    
    public func sorted<Value>(
        by keyPath: KeyPath<Self.Element, Value>,
        using valuesAreInIncreasingOrder: (Value, Value) throws -> Bool)
        rethrows -> [Self.Element]
    {
        return try self.sorted(by: {
            try valuesAreInIncreasingOrder($0[keyPath: keyPath], $1[keyPath: keyPath])
        })
    }
    
    public func sorted<Value: Comparable>(
        by keyPath: KeyPath<Self.Element, Value>)
        -> [Self.Element]
    {
        return self.sorted(by: keyPath, using: <)
    }
    
}
extension MutableCollection where Self: RandomAccessCollection {
    // an equivalent set of methods, but `public mutating func sort(by:...) { ... }`
}
I'd love to hear other people's thoughts on this! I'm aware there's a high bar for accepting new methods or method-variants into the Standard Library, but this seems like a great application of the (relatively new) KeyPath API.
Additional Directions
We can also use Key Paths to drive a new SortDescriptor API:
OS.supported.sorted(by: [
    SortDescriptor(\.yearReleased),
    SortDescriptor(\.name, using: String.caseInsensitiveCompare)])
// instead of
OS.supported.sorted { lhs, rhs in
    if lhs.yearReleased == rhs.yearReleased {
        return lhs.name.caseInsensitiveCompare(rhs.name) == .orderedAscending
    } else {
        return lhs.yearReleased < rhs.yearReleased
    }
}
I described one potential implementation for an API like that a bit down-thread.
Alternatives
If we want symmetry with methods like map and filter, we could potentially use (Self.Element) -> Comparable instead so that it would read like OS.supported.sorted(by: { $0.yearReleased }) and SortDescriptor({ $0.name }, using: String.caseInsensitiveCompare).
