SE-0249 introduced the ability to use keypaths for a certain method signature interchangably with functions with the same signature as function arguments. So, for instance, the line users.map( { $0.email })
can be interchanged with the line users.map(\.email)
.
What I would like to propose is that, through no change to the language itself, but only some additions to the standard library, to allow keypaths to be used in certain Sequence
methods that accept a closure with two parameters of type Sequence.Element
, such as (Element, Element) -> Bool
as used by Sequence.sort
, Sequence.min
and Sequence.max
.
Such a method would allow for briefer code with less repetition for most uses of sort/sorted
, min
and max
, which are usually written as sorted { $0.count < $1.count }
or max { $0.weight < $1.weight }
, accessing the same property on both the left hand and right hand side of the comparison operator.
Using keypaths instead, the line sorted { $0.count < $1.count }
could instead be written as sorted(by: \.count)
, with the sorted(by keyPath: KeyPath<Element>) -> [Element]
method calling the current sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element]
method. An optional extra parameter could supply a custom areInIncreasingOrder
function for comparing elements.
A simplistic implementation of sort
, min
and max
using keypaths could look like this:
extension Sequence {
/// Returns the elements of the sequence, sorted by a (`Comparable`) property indicated by the keypath, optionally using the given predicate as the comparison between elements.
func sorted<T: Comparable>(by keyPath: KeyPath<Self.Element, T>, using sortingFunction: (T, T) -> Bool = (<)) -> [Self.Element] {
self.sorted(by: { sortingFunction($0[keyPath: keyPath], $1[keyPath: keyPath]) })
}
/// Returns the maximum element in the sequence, using the given predicate as the comparison between elements.
func max<T: Comparable>(by keyPath: KeyPath<Self.Element, T>, using comparingFunction: (T, T) -> Bool = (<)) -> Self.Element? {
self.max(by: { comparingFunction($0[keyPath: keyPath], $1[keyPath: keyPath]) })
}
/// Returns the minimum element in the sequence, using the given predicate as the comparison between elements.
func min<T: Comparable>(by keyPath: KeyPath<Self.Element, T>, using comparingFunction: (T, T) -> Bool = (<)) -> Self.Element? {
self.min(by: { comparingFunction($0[keyPath: keyPath], $1[keyPath: keyPath]) })
}
}
This is primarily a quality-of-life pitch, but I think it fits in with the pattern established by SE-0249 and would allow for cleanlier map/flatMap/reduce
pipelines by reducing repetition and a certain amount of visual clutter, just as map(\Root.keyPath)
has done.
EDIT:
As @ole has pointed out, similar pitches have been made before, notably by @cal in Sort `Collection` using `KeyPath` (which was pitched before using keypaths in place of closures for higher-order functions made it into the language).
In a follow-up comment posted three years later (after the above change came into effect), @olbo points out that the interchangability of keypaths and closures means that the function can be generalised to accept either form. This means that the implementation of sorted
mentioned above can be changed from:
func sorted<T: Comparable>(by keyPath: KeyPath<Self.Element, T>, using sortingFunction: (T, T) -> Bool = (<)) -> [Self.Element] {
self.sorted(by: { sortingFunction($0[keyPath: keyPath], $1[keyPath: keyPath]) })
}
into:
func sorted<T: Comparable>(by keyPath: (Self.Element) -> T, using sortingFunction: (T, T) -> Bool = (<)) -> [Self.Element] {
self.sorted(by: { sortingFunction(keyPath($0), keyPath($1)) })
}
@lovee has also pitched a form like this for min
/max
in Sort(by:) min(by:) max(by:) with keyPaths where he mentions two potential forms for calling the max
function:
let max = sequence.max(by: \.property)
or
let max = sequence.max(by: { $0.getSomeValue(from: anotherData) })
.
Finally, @anthonylatsis proposed Map Sorting in 2019, with the added twist of an isExpensiveTransform
parameter to tell the sorting algorithm to cache compared values in case they may be expensive to fetch, as in the let max = sequence.max(by: { $0.getSomeValue(from: anotherData) })
example above where getSomeValue
may be called repeatedly during the sort. @cal also contributed to that thread with some statistics from the Swift Compatibility Suite regarding typical use cases of sort
and sorted
, and what optional comparison function was supplied.
That proposal never made it into review, with a follow-up comment:
The discussion has since stalled, and I would appreciate input from other Swift users here.