In describing a sort by a property (a la people.sorted(by: { $0.name })
), the layer of convenience performs one task: the transformation of a property access of type (Element) -> SortKey
to a comparison function of type (Element, Element) -> Bool
.
Here's another way of looking at the same transformation: given a function of type (SortKey, SortKey) -> Bool
(e.g. <
) and a function of type (Element) -> SortKey
(e.g. { $0.name }
), we can use the latter to lift the former into a function of type (Element, Element) -> Bool
(in this case, { $0.name < $1.name }
).
We can write a function, which I'll call their
for readability purposes made clear in a moment, which performs this lift:
func their<Input, Intermediate, Output>(
_ extractIntermediate: @escaping (Input) -> Intermediate,
_ combine: @escaping (Intermediate, Intermediate) -> Output
) -> (Input, Input) -> Output {
return { input1, input2 in
combine(extractIntermediate(input1), extractIntermediate(input2))
}
}
Then, using only the existing sorting API, we can write the following:
people.sorted(by: their({ $0.name }, <)) // equivalent to `{ $0.name < $1.name }`
people.sorted(by: their({ $0.name }, >)) // equivalent to `{ $0.name > $1.name }`
You can picture how much prettier the above syntax would become with the appropriate subtyping relationship between KeyPath<T, U>
and (T) -> U
:
people.sorted(by: their(\.name, <))
There are additional gains to writing the function in this way, however. Notice that their
isn't restricted to Comparable
SortKey
types, which means we can make use of tuple comparisons even without tuple protocol conformances:
people.sorted(by: their({ ($0.name, $0.age, $0.height, $0.salary) }, <))
Tuple comparison only covers the case when all desired properties are sorted by the same comparison function—Ben's proposed API utilizing a Sequence
of comparison functions fills the gap here, and it works with their
, too:
people.sorted(by: [
their({ $0.name }, <),
their({ $0.age }), >)
])
Finally, I'll note that this function generalizes beyond sorting. Here a couple additional examples:
let oldestPerson = people.max(by: their({ $0.age }, <))
// `their` isn't tied to functions returning `Bool`, either:
let sequentialRunTimeDeltas = zip(firstRuns, secondRuns).map(their({ $0.duration }, -))
Whether their
is appropriate for this particular proposal, I'm not sure—but I would be remiss not to mention it for its utility in creating comparison functions without significantly expanding API surface.
A quick note on prior art
Haskell's Data.Function package defines on
, which you can see here. on
is basically an infix version of their
which better lends to readability given the names of functions with which it's commonly used in Haskell.
I'd imagine other functional languages define a similar function, though I haven't done much digging here.