Continuing on with my last post, I want to look at another function which I consider particularly elegant, the psi combinator:
const psi = (f) => (g) => (x, y) => g(f(x), f(y));
In TypeScript:
const psi = <A, B>(f: (x: A) => B) =>
<C>(g: (x: B, y: B) => C) =>
(x: A, y: A): C =>
g(f(x), f(y));
This is also called on
in Haskell.
What it does is map a function over both arguments of a binary(two-argument) function. This is similar to the B
combinator but changed to work on binary functions.
The quintessential usage is when sorting records:
// given a compare function
const localeCompare = (item1: string, item2: string): number =>
item1.localeCompare(item2);
// and some accessor function that drills into a data structure
const getName = (person) => person.name;
// compose that accessor with the compare function to drill both sides
const compareNames = psi(getName)(localeCompare);
// which can be passed to a sort method for an array of that structure
people.sort(compareNames)
Interestingly, this is equivalent to doing a map and then sort, but using psi is theoretically more memory efficient:
// generates an extra array
people.map(getName).sort(localeCompare)
Look out for other opportunities to use psi
and I'm sure you'll find them. Particularly if you're doing data aggregation or processing.
Pedantic Disclaimer: Psi is usually defined with the binary function as the first argument but I preferred the similarity to B
combinator when taking the mapping function as the first.
Top comments (1)
If you want to use this with your team you may find the name "on" to be more palatable.
or uncurrying it
though this makes it less flexible.