Creating Composable Sorting Hierarchies in Typescript

Reid Evans
3 min readMay 22, 2018

--

“gray and brown rocks” by Markus Spiske on Unsplash

This post draws heavy influence from an incredible post from Brian Lonsdorf. The goal will be to define primitive sorting rules and and combine them to create sorting rules for more complex structures.

So the first thing we need to do is to have a way of specifying a comparison so that we can sort. To do this so that we can integrate nicely with Array.sort we use an enum with specified values of 1, -1, and 0 respectively. We’ll also create a generic interface defining what it means to compare two values.

As you can see, we now have the interface structure we need and some example instances of our Ord interface with ascending and descending. Those instances can be read as “ascending and descending are descriptions of how to order numbers”.

There are a couple of ways to use our instance. We can actually use our instance with the default Array.sort method by passing the compare function from our instance like so

We can also create a sort function that takes an A[] and an Ord<A> and sorts the array.

A second look at Descending

Looking back at our ascending and descending functions, it seems like a lot of similar code between them, and we probably won’t want to write those for every type. Luckily, now that we have our Ord<A> interface as an abstraction point for ordering, we can actually write a function that will produce the inverse ordering!

Sweet, now we’ve written descending in terms of ascending so that we don’t have to squint our eyes to determine whether we have our angle brackets pointing the correct way!

Our invert function just switches the order of arguments (thanks to gcanti for the much simpler implementation). Because our invert function relies only on the generic interface, it works for any instance of Ord<A>. Have an Ord<Date> or Ord<LiterallyAnyOtherType> that you want in descending order? No problem, just wrap it with invert and you’re done!

Composable Hierarchies

Now that we have our abstraction let’s see how we can make it composable. This is where our friend Closure again comes in. We want to be able to take two Ord<A> instances and combine them together to return another Ord<A> instance. This is the basis of composable architecture.

Our appendOrdering function takes two Ordering values and returns the first if it is not equal else it returns the second. This works like “sort by x and then y”.

Next we define append for Ord<A> which compares the values with the first and second Ord<A> instances and delegates off to appendOrdering to complete our “sort by this and then that” concept.

While that gets a bit complex and abstract, the important thing to remember here is that this code works for all Ord<A> instances, so this code only ever has to exist once.

Let’s see it in use to get a better intuition for how this all fits together.

Look at that beautiful composition of Ord<Person> instances! Our initial sort function still works and we could easily extend to include any other property that might exist on a more complex Person interface.

Contravariance to the rescue

As beautiful as our composition was, our Ord<Person> instances were pretty ugly. Basically we’re just comparing string properties on a Person object, but we’ve had to define string comparison on a specific property of a specific type. That seems very limited and a recipe for future boilerplate. Luckily we can do better with the power of contramap!

Our contramap function takes a function from B -> A and an Ord<A> and returns an Ord<B>! That means we can take primitive sorting algorithms and extend them to work for more complex types. contramap just applies the function to the values before comparing them.

In this article we’ve seen how to define ordering rules and combine them in a couple of different ways. We can now take primitive sorting rules and combine them, invert them, and extend them to work on more complex types. Just look at how easily we were able to define firstThenLastDescending!

--

--