I'm trying to implement a convenience Collection.sorted(by: KeyPath)
function.
So far, it works if do
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
return sorted { lhs, rhs
return lhs[keyPath: keyPath] < rhs[keyPath: keyPath]
}
}
But what if I want to allow the caller to specify the actual sorting logic ? I added a callback to perform the comparison, like such (taking inspiration from the orginal sorted(_:)
function signature).
func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>, _ compare: (T, T) throws -> Bool) rethrows -> [Element] {
return try sorted { lhs, rhs in
return try compare(lhs[keyPath: keyPath], rhs[keyPath: keyPath])
}
}
Now, this is all works, but it means the callsite always has to specify which sorting operation to perform.
let sorted = myArray.sorted(by: \.name, <)
I'd like it to default to <
, but how can I reference the <
operator by default, in my function's signature ?
1 Like
sveinhal
(Svein Halvor Halvorsen)
March 20, 2020, 11:45am
2
extension Sequence {
func sorted<T: Comparable>(
by keyPath: KeyPath<Element, T>,
_ compare: (T, T) throws -> Bool = { $0 < $1 }
) rethrows -> [Element] {
return try sorted { lhs, rhs in
return try compare(lhs[keyPath: keyPath], rhs[keyPath: keyPath])
}
}
}
2 Likes
AlexanderM
(Alexander Momchilov)
March 20, 2020, 2:03pm
4
You can reference the operator as an unapplied function by wrapping it ( )
:
extension Sequence {
func sorted<T: Comparable>(
by keyPath: KeyPath<Element, T>,
_ compare: (T, T) throws -> Bool = (<)
) rethrows -> [Element] {
return try sorted { try compare($0[keyPath: keyPath], $1[keyPath: keyPath]) }
}
}
6 Likes
That's what I was looking for ! Awesome, thank you so much
jrose
(Jordan Rose)
March 20, 2020, 7:49pm
6
As an aside, it's worth noting that the "regular" sorted(by:)
(that takes a comparator) doesn't require Comparable . Unfortunately, having those differing requirements means that sorted(by:)
doesn't just have a default paramater; it has to be two separate methods. You'll have to decide whether you want your library to do the same.
Swift 5.2 makes it so you can use the same extension for key paths and closures.
func sorted<Comparable: Swift.Comparable>(
_ getComparable: (Element) throws -> Comparable,
_ getAreInIncreasingOrder: (Comparable, Comparable) throws -> Bool = (<)
) rethrows -> [Element] {
try sorted {
try getAreInIncreasingOrder( getComparable($0), getComparable($1) )
}
}
The problem with this method is that, even though <
doesn't throw, the compiler will still enforce you to use try
at the call site.
I don't know if that's worthy of a bug report.
As it is, two overloads are the way to go.
func sorted<Comparable: Swift.Comparable>(
_ getComparable: (Element) throws -> Comparable
) rethrows -> [Element] {
try sorted(getComparable, <)
}
func sorted<Comparable: Swift.Comparable>(
_ getComparable: (Element) throws -> Comparable,
_ getAreInIncreasingOrder: (Comparable, Comparable) throws -> Bool
) rethrows -> [Element] {
try sorted {
try getAreInIncreasingOrder( getComparable($0), getComparable($1) )
}
}
jrose
(Jordan Rose)
March 20, 2020, 7:58pm
8
anon9791410:
The problem with this method is that, even though <
doesn't throw, the compiler will still enforce you to use try
at the call site.
I don't know if that's worthy of a bug report.
Seems bug-report-worthy to me. From what I know about how the compiler handles default arguments I don't think it'll be easy to fix (try
checking is handled before default arguments are "expanded"), but IMHO it's still worth filing.
Submitted ! For reference, here's the simplest example of the problem I could come up with:
func ƒ( _: () throws -> Void = { } ) rethrows { }
ƒ { } // compiles.
ƒ() // "Call can throw but is not marked with 'try'"
6 Likes
jrose:
As an aside, it's worth noting that the "regular" sorted(by:)
(that takes a comparator) doesn't require Comparable . Unfortunately, having those differing requirements means that sorted(by:)
doesn't just have a default paramater; it has to be two separate methods. You'll have to decide whether you want your library to do the same.
To me that sounds like a pretty good reason to have two methods.
Thanks :) As a note however, some googling quickly showed that a similar report had already been submitted quite a while ago: [SR-1534] Error: Call is to rethrows function, but a defaulted argument function can throw · Issue #44143 · apple/swift · GitHub
Thanks all for the input