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
$0
and$1
is 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)
.