Projection in collections methods

I have code that's like the following:

struct MyData {
   let time: Date
}
let my_data: [MyData] = ...
let sorted_data = my_data.sorted(by: { $0.time > $1.time })

This works fine, but I was wondering if there was something akin to "Projections" in C++:

struct MyData {
   uint64_t time;
};
vector<MyData> my_data = ...;
ranges::sort(my_data, {}, &MyData::time);
};

I would imagine in swift you'd use some generic function that took a predicate and a key path and returned another predicate

You just need another sorted overload.

let now: Date = .now
let myData: [MyData] = [
  .init(time: .distantFuture),
  .init(time: .distantPast),
  .init(time: now)
]

#expect(
  myData.sorted(by: \.time).map(\.time) == [
    .distantPast, now, .distantFuture
  ]
)
public extension Sequence {
  @inlinable func sorted<each Comparable: Swift.Comparable, Error>(
    by comparable: (Element) throws(Error) -> (repeat each Comparable)
  ) throws(Error) -> [Element] {
    do {
      return try sorted { try (repeat each comparable($0)) < (repeat each comparable($1)) }
    } catch {
      throw error as! Error
    }
  }
}

@inlinable public func < <each Element: Comparable>(
  _ element0: (repeat each Element),
  _ element1: (repeat each Element)
) -> Bool {
  for elements in repeat (each element0, each element1) {
    if elements.0 < elements.1 { return true }
    if elements.0 > elements.1 { return false }
  }
  return false
}

(I've tried to figure out how/if you could pass in >, instead of having to reverse the sorted array, but the compiler gets crashy with parameter packs. It's easy if you don't use packs.)

let sortedDates = myData.sorted(by: \.time).map(\.time)
#expect(sortedDates == [.distantPast, now, .distantFuture])
#expect(myData.sorted(by: \.time, >).map(\.time) == sortedDates.reversed())
public extension Sequence {
  @inlinable func sorted<Comparable: Swift.Comparable, Error>(
    by comparable: (Element) throws(Error) -> Comparable,
    _ areInIncreasingOrder: (Comparable, Comparable) throws(Error) -> Bool = (<)
  ) throws(Error) -> [Element] {
    do {
      return try sorted {
        try areInIncreasingOrder(comparable($0), comparable($1))
      }
    } catch {
      throw error as! Error
    }
  }

  // This should be covered by the other overload, 
  // but using that one with `Never` crashes the compiler.
  @inlinable func sorted<Comparable: Swift.Comparable>(
    by comparable: (Element) -> Comparable,
    _ areInIncreasingOrder: (Comparable, Comparable) -> Bool = (<)
  ) -> [Element] {
    sorted {
      areInIncreasingOrder(comparable($0), comparable($1))
    }
  }
}

Also keep in mind that KeyPath access might have a performance overhead you don't have to pay with direct member access. I believe KeyPath access on struct types should be pretty fast… but last I remember there were some TODO optimizations missing for KeyPath access on class types.

You can also use my_data.sorted(using: KeyPathComparator(\.time)).

Yes but that does require Foundation.

I use something very similar in my projects moreso for puzzle scenarios like AoC. I made the latter method quickly just now to parallel existing sorted(by:).

func sorted<Key: Comparable>(using keyPath: KeyPath<Element, Key>) -> [Element] {
    sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
}

func sorted<Key: Comparable>(
    using keyPath: KeyPath<Element, Key>,
    by areInIncreasingOrder: (Key, Key) throws -> Bool
) rethrows -> [Element] {
    try sorted(by: { try areInIncreasingOrder($0[keyPath: keyPath], $1[keyPath: keyPath]) })
}

This only supports key paths. Key paths can be used as closures as of Swift 5.2, so it is generally never important to support them directly.

Sorting is also O(n log n). Memoization can be an important optimization to have available. KeyPath conforms to Hashable. It is easy to confirm if two KeyPaths have changed over time (and the sorted results should be recomputed). With two closures… there isn't really any stable and easy concept of "value equality" you can leverage to determine if the user changed the sort order.