Skwiggs
(Dorian)
March 20, 2020, 11:16am
#1
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
Skwiggs
(Dorian)
March 20, 2020, 2:19pm
#5
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.
Jessy
(Jessy)
March 20, 2020, 7:55pm
#7
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
Jessy:
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.
Jessy
(Jessy)
March 20, 2020, 8:19pm
#9
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.
Skwiggs
(Dorian)
March 23, 2020, 10:10am
#11
Thanks :) As a note however, some googling quickly showed that a similar report had already been submitted quite a while ago: https://bugs.swift.org/browse/SR-1534
Thanks all for the input