After George Wilson’s talk on Monoids I mentioned this 18 year old reddit post about using Monoids for sorting; a BFPG friend requested a modern writeup given that Monoid split into Semigroup and Monoid in GHC 8.4.

The trick hasn’t changed, but the classes have, so let’s start from the current definitions and work towards our goal of sorting on arbitrary attributes of a type.

class Semigroup a where
  (<>) :: a -> a -> a    -- must be associative!

class Semigroup a => Monoid a where
  mempty  :: a              -- identity for (<>)
  mappend :: a -> a -> a    -- legacy, should equal (<>)

  mconcat :: [a] -> a -- fold a list with <>, starting from mempty

Semigroup is for things that can be combined with an associative operation, i.e.

forall a, b, c => a <> (b <> c) == (a <> b) <> c 

For String, the Semigroup instance is normal concatenation:

instance Semigroup String where
  x <> y = x ++ y

With the empty string "" we can define a Monoid instance:

instance Monoid String where
  mempty = "" -- mempty <> x = x, x <> mempty = x for all x

We shouldn’t let this colour our thinking: there are many values in the type String, but we can define Semigroup and Monoid instances on a type with just one value, the unit type ().

instance Semigroup () where
  () <> () = ()

instance Monoid () where
  mempty = ()

What about a type with two values? The integers under addition modulo 2:

data Z2 = N0 | N1

instance Semigroup Z2 where
  N0 <> N0 = N0
  N0 <> N1 = N1
  N1 <> N0 = N1
  N1 <> N1 = N0

instance Monoid Z2 where
  mempty = N0

What about a type with three values? We could do integers under addition modulo 3, but instead let’s look at the Ordering type for ordering:

data Ordering = LT | EQ | GT

instance Semigroup Ordering where
  -- LT wins
  LT <> EQ = LT
  LT <> LT = LT
  LT <> GT = LT

  -- GT wins
  GT <> EQ = GT
  GT <> LT = GT
  GT <> GT = GT

  -- EQ is a tie, so use the other value
  EQ <> EQ = EQ
  EQ <> LT = LT
  EQ <> GT = GT

instance Monoid Ordering where
  mempty = EQ

Does this make sense? Let’s check associativity, brute force:

-- a = LT: LT on the left wins straight away, so both sides are LT.
(LT <> LT) <> LT == LT <> (LT <> LT)   -- LT <> LT == LT <> LT   -- LT == LT
(LT <> LT) <> EQ == LT <> (LT <> EQ)   -- LT <> EQ == LT <> LT   -- LT == LT
(LT <> LT) <> GT == LT <> (LT <> GT)   -- LT <> GT == LT <> LT   -- LT == LT
(LT <> EQ) <> LT == LT <> (EQ <> LT)   -- LT <> LT == LT <> LT   -- LT == LT
(LT <> EQ) <> EQ == LT <> (EQ <> EQ)   -- LT <> EQ == LT <> EQ   -- LT == LT
(LT <> EQ) <> GT == LT <> (EQ <> GT)   -- LT <> GT == LT <> GT   -- LT == LT
(LT <> GT) <> LT == LT <> (GT <> LT)   -- LT <> LT == LT <> GT   -- LT == LT
(LT <> GT) <> EQ == LT <> (GT <> EQ)   -- LT <> EQ == LT <> GT   -- LT == LT
(LT <> GT) <> GT == LT <> (GT <> GT)   -- LT <> GT == LT <> GT   -- LT == LT

-- a = EQ: EQ defers to whatever b <> c decides.
(EQ <> LT) <> LT == EQ <> (LT <> LT)   -- LT <> LT == EQ <> LT   -- LT == LT
(EQ <> LT) <> EQ == EQ <> (LT <> EQ)   -- LT <> EQ == EQ <> LT   -- LT == LT
(EQ <> LT) <> GT == EQ <> (LT <> GT)   -- LT <> GT == EQ <> LT   -- LT == LT
(EQ <> EQ) <> LT == EQ <> (EQ <> LT)   -- EQ <> LT == EQ <> LT   -- LT == LT
(EQ <> EQ) <> EQ == EQ <> (EQ <> EQ)   -- EQ <> EQ == EQ <> EQ   -- EQ == EQ
(EQ <> EQ) <> GT == EQ <> (EQ <> GT)   -- EQ <> GT == EQ <> GT   -- GT == GT
(EQ <> GT) <> LT == EQ <> (GT <> LT)   -- GT <> LT == EQ <> GT   -- GT == GT
(EQ <> GT) <> EQ == EQ <> (GT <> EQ)   -- GT <> EQ == EQ <> GT   -- GT == GT
(EQ <> GT) <> GT == EQ <> (GT <> GT)   -- GT <> GT == EQ <> GT   -- GT == GT

-- a = GT: GT on the left wins straight away, so both sides are GT.
(GT <> LT) <> LT == GT <> (LT <> LT)   -- GT <> LT == GT <> LT   -- GT == GT
(GT <> LT) <> EQ == GT <> (LT <> EQ)   -- GT <> EQ == GT <> LT   -- GT == GT
(GT <> LT) <> GT == GT <> (LT <> GT)   -- GT <> GT == GT <> LT   -- GT == GT
(GT <> EQ) <> LT == GT <> (EQ <> LT)   -- GT <> LT == GT <> LT   -- GT == GT
(GT <> EQ) <> EQ == GT <> (EQ <> EQ)   -- GT <> EQ == GT <> EQ   -- GT == GT
(GT <> EQ) <> GT == GT <> (EQ <> GT)   -- GT <> GT == GT <> GT   -- GT == GT
(GT <> GT) <> LT == GT <> (GT <> LT)   -- GT <> LT == GT <> GT   -- GT == GT
(GT <> GT) <> EQ == GT <> (GT <> EQ)   -- GT <> EQ == GT <> GT   -- GT == GT
(GT <> GT) <> GT == GT <> (GT <> GT)   -- GT <> GT == GT <> GT   -- GT == GT

All 27 cases agree, so <> on Ordering really is associative. The first non-EQ value decides the result, and that is true no matter where the parentheses go.

Similarly, for Monoid:

-- Left identity: mempty <> x == x, with mempty = EQ.
mempty <> LT == LT   -- EQ <> LT == LT   -- LT == LT
mempty <> EQ == EQ   -- EQ <> EQ == EQ   -- EQ == EQ
mempty <> GT == GT   -- EQ <> GT == GT   -- GT == GT

-- Right identity: x <> mempty == x.
LT <> mempty == LT   -- LT <> EQ == LT   -- LT == LT
EQ <> mempty == EQ   -- EQ <> EQ == EQ   -- EQ == EQ
GT <> mempty == GT   -- GT <> EQ == GT   -- GT == GT

All 6 cases agree, so EQ is an identity for <> on both sides, and Ordering is a Monoid. Checking both sides matters here because <> on Ordering is not commutative: LT <> GT is LT but GT <> LT is GT.

Sorting people Link to heading

Suppose we have a record type for a person:

data Person = Person 
    { name      :: String
    , age       :: Int
    , country   :: String
    }

We are asked to sort by name, then age, then country (basically a dictionary style sorting).

Here’s our first attempt, fully manual:

byNameThenAgeThenCountry :: Person -> Person -> Ordering
byNameThenAgeThenCountry (Person n0 a0 c0) (Person n1 a1 c1)
    -- 1. Compare names
    | n0 < n1
    = LT

    | n0 > n1
    = GT

    -- 2. Names tie: compare ages
    | n0 == n1
    , a0 < a1
    = LT

    | n0 == n1
    , a0 > a1
    = GT

    -- 3. Names and ages tie: compare countries
    | n0 == n1
    , a0 == a1
    , c0 < c1
    = LT

    | n0 == n1
    , a0 == a1
    , c0 > c1
    = GT

    -- 4. Everything ties
    | otherwise
    = EQ

This is agony. Too many cases, too easy to get things wrong, and it doesn’t scale to more comparison fields. But it does work with the standard library sortBy function:

import Data.List ( sortBy )

sortBy :: (a -> a -> Ordering) -> [a] -> [a]

sortByNameThenAgeThenCountry :: [Person] -> [Person]
sortByNameThenAgeThenCountry = sortBy byNameThenAgeThenCountry 

Wouldn’t it be nicer if we could “lift” a function Person -> u to a comparison function Person -> Person -> Ordering? And then combine them with <>? How would this work?

By definition, the records give us functions

name      :: Person -> String
age       :: Person -> Int
country   :: Person -> String

So we need something like

lift :: (Person -> u) -> Person -> Person -> Ordering

which would let us write

sortBy (lift name)
sortBy (lift age)
sortBy (lift country)

So this lift function must be

lift :: (t -> u) -> t -> t -> Ordering -- won't compile yet...
lift f x y
  | f x < f y = LT
  | f x > f y = GT
  | otherwise = EQ

We have actually discovered comparing from Data.Ord (and realise that we need to be able to compare the values, hence an Ord constraint!).

comparing :: Ord a => (b -> a) -> b -> b -> Ordering

So where are we? We can write

comparing name      :: Person -> Person -> Ordering
comparing age       :: Person -> Person -> Ordering
comparing country   :: Person -> Person -> Ordering

and we can use them with sortBy:

sortBy (comparing name)      :: [Person] -> [Person]
sortBy (comparing age)       :: [Person] -> [Person]
sortBy (comparing country)   :: [Person] -> [Person]

All good so far!

Returning to our original problem, wouldn’t it be nice if we could combine the comparing f forms?

nameAgeCountry = comparing name ~:~ comparing age ~:~ comparing country

and use this as sortBy nameAgeCountry? Then we can do different orderings like

sortBy (comparing name ~:~ comparing age)
sortBy (comparing name ~:~ comparing country ~:~ comparing age)
sortBy (comparing age  ~:~ comparing country ~:~ comparing name)

What’s the type of this magical combinator ~:~? It must be

(~:~) :: (Person -> Person -> Ordering)
      -> (Person -> Person -> Ordering)
      -> (Person -> Person -> Ordering)

Squint at that type. Writing Cmp for Person -> Person -> Ordering, it’s

type Cmp = Person -> Person -> Ordering

(~:~) :: Cmp -> Cmp -> Cmp

which is exactly the shape of (<>) :: a -> a -> a with a = Cmp. So what we’re really asking for is a Semigroup instance for the function type Person -> Person -> Ordering.

Let’s try to write it. We’re handed two comparison functions f and g and two people x and y. The only sensible thing to do is run both comparisons:

f x y :: Ordering
g x y :: Ordering

and then combine the two results: if f has an opinion, go with it, otherwise fall back to g. “First non-EQ wins” is precisely the <> on Ordering from the start of this post. So:

(f ~:~ g) x y = f x y <> g x y

That’s the whole combinator.

Notice that it does not use anything about Person. And it doesn’t even care about Ordering, except that it has a <> operation. So we can generalise: any function into a Semigroup is itself a Semigroup, combined pointwise:

instance Semigroup b => Semigroup (a -> b) where
  (f <> g) x = f x <> g x

Read Person -> Person -> Ordering as Person -> (Person -> Ordering) and apply the instance twice: Ordering is a Semigroup, so Person -> Ordering is a Semigroup, so Person -> Person -> Ordering is a Semigroup. Unfolding:

(f <> g) x y = (f x <> g x) y = f x y <> g x y

which is exactly ~:~.

Is it associative? Pick any x and y and unfold both sides:

((f <> g) <> h)  x y = (f <> g) x y <> h x y = (f x y <>  g x y) <> h x y
(f  <> (g <> h)) x y = f x y <> (g <> h) x y =  f x y <> (g x y  <> h x y)

The right-hand sides are three Ordering values bracketed in two different ways, and we manually checked the 27 cases earlier. Associativity of functions-into-Ordering is inherited from associativity of Ordering.

The same trick gives mempty:

instance Monoid b => Monoid (a -> b) where
  mempty = \_ -> mempty

On our type, this is mempty = \_ _ -> EQ, the comparison that thinks everybody ties, and it’s an identity for the same reason:

(mempty <> f) x y = EQ <> f x y = f x y
(f <> mempty) x y = f x y <> EQ = f x y

So our combinator ~:~ is actually <> from Semigroup! The agonising function at the top of this section becomes

import Data.List ( sortBy )
import Data.Ord  ( comparing )

sortByNameThenAgeThenCountry :: [Person] -> [Person]
sortByNameThenAgeThenCountry = sortBy (comparing name <> comparing age <> comparing country)

The reader might say, eh, why not just map to tuples? Indeed we could:

sortOn (\p -> (name p, age p, country p))

using the other function sortOn in Data.List.

However the “encoding” of the sorting decision is now hidden inside a lambda expression.

In contrast, our comparators are ordinary values. We can put them in a list and fold them together with mconcat:

import Data.Ord ( comparing, Down(..) )

nameAgeCountry :: [Person -> Person -> Ordering]
nameAgeCountry = [comparing name, comparing (Down . age), comparing country]

sortByNameThenAgeThenCountry :: [Person] -> [Person]
sortByNameThenAgeThenCountry = sortBy (mconcat nameAgeCountry)

Reordering the sort keys is now reordering a list. Down flips the direction of a single key, so this sorts by name, then oldest first, then country. And because the list is just data, we can build it at runtime, say from the columns a user clicked on in a table. The empty list of keys is mconcat [], which is mempty.

And maybe the coolest sort function of them all, sortBy mempty compares nothing, so it leaves the list exactly as it found it.

Loading comments…

Comment form pending Cloudflare verification...