Reference default comparison function as a function parameter

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
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

Oh. okay :laughing:
Thanks

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

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. :trophy:

  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) )
    }
  }

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

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 :muscle: