Irakli Safareli

PSST… Ordering is a Monoid


What is a Monoid

if you have this 3 things T, concat and empty together, you have a monoid:

We can express this in typescript like this:

type Monoid<T> = {
  concat: (a: T, b: T) => T;
  empty: T;
};

And we can have this laws/tests that must hold true for valid monoids:

const associativity = <T>(M: Monoid<T>, a:T, b:T, c: T) =>
  deepEqual(
    M.concat(a, M.concat(b, c)),
    M.concat(M.concat(a, b), c))
  )

const leftIdentity = <T>(M: Monoid<T>, a:T) =>
  deepEqual(
    M.concat(a, M.empty),
    a
  )

const rightIdentity = <T>(M: Monoid<T>, a:T) =>
  deepEqual(
    M.concat(empty, M.a),
    a
  )

Why should you care? why are discovering such structures useful?

The fact that you can refactor a _ (b _ c) into a _ b _ c and be sure that result will not change, or the reason you can write a + b + c and don’t care if you even use parentheses at all, is + and _ are associativity. The reason you would refactor a + 0 or a _ 1 to just a is because 0 is identity to the + operator as well as1 for *. Once you know something is a monoid you know that if you want to concat multiple values you can group in multiple batches execute them on multiple threads/workers and combine them. You can implement generic functions for such structures my favorite is fold

function fold<T>(M: Monoid<T>, array: T[]): T {
  return array.reduce(M.concat, M.empty);
}

fold(stringMonoid, ["a", "b", "c"]) === "abc";
fold(arrayMonoid, [["a"], ["b"], ["c"]]) === ["a", "b", "c"];
fold(numberAdditiveMonoid, [1, 2, 3]) === 6;
fold(numberMultiplicativeMonoid, [2, 3, 4]) === 24;

Here are Some Monoid Examples

one of the most obvious instance are for String and Array

String

const stringMonoid: Monoid<string> = {
  concat: (a: string, b: string): string => a.concat(b),
  empty: "",
};

// Associativity
a.concat(c.concat(d)) === a.concat(c).concat(d);

// Identity
"".concat(s) === s;
s.concat("") === s;

Array

const arrayMonoid: Monoid<Array<any>> = {
  concat: function<T>(a:Array<T>, b:Array<T>):Array<T> {
    return a.concat(b)
  },
  empty: [],
}

// Associativity
a.concat(c.concat(d)) === (a.concat(c)).concat(d)

// Identity
[].concat(s) === s
s.concat([]) === s

Note that for some type T there can exist more then one Monoid

Most well known of such types is Number

Number

const numberAdditiveMonoid: Monoid<number> = {
  concat: (a: number, b: number): number => a + b,
  empty: 0,
};

// Associativity
a + (b + c) === a + b + c;

// Identity
0 + s === s;
s + 0 === s;

and

const numberMultiplicativeMonoid: Monoid<number> = {
  concat: (a: number, b: number): number => a * b,
  empty: 1,
};

// Associativity
a * (b * c) === a * b * c;

// Identity
1 * s === s;
s * 1 === s;

Boolean

const booleanAndMonoid: Monoid<boolean> = {
  concat: (a: boolean, b: boolean): boolean => a && b,
  empty: true,
};

// Associativity
a && (b && c) === (a && b) && c;
// Identity
true && s === s;
s && true === s;

const booleanOrMonoid: Monoid<boolean> = {
  concat: (a: boolean, b: boolean): boolean => a || b,
  empty: false,
};

// Associativity
a || (b || c) === (a || b) || c;
// Identity
false || s === s;
s || false === s;

Ordering

Ordering is my favorite, it’s very nice and useful, when you want to sort array of some records based on many properties.

type Ordering = -1 | 0 | 1

const orderingMonoid: Monoid<Ordering> =  {
  concat: (a: Ordering, b: Ordering): Ordering {
    return a === 0 ? b : a
  },
  empty: 0
}

const users =
  [ { firstName:"Alisa", lastName:"Able" }
  , { firstName:"Bob",   lastName:"Able" }
  , { firstName:"Alisa", lastName:"Brown" }
  , { firstName:"Bob",   lastName:"Brown" }
  , { firstName:"Alisa", lastName:"Bold" }
  , { firstName:"Bob",   lastName:"Bold" }
  ]

users.sort(
  (a,b) => orderingMonoid.concat(
    a.firstName.localeCompare(b.firstName),
    a.lastName.localeCompare(b.lastName)
  )
)

// users now will be
[ { "firstName": "Alisa", "lastName": "Able" }
, { "firstName": "Alisa", "lastName": "Bold" }
, { "firstName": "Alisa", "lastName": "Brown" }
, { "firstName": "Bob", "lastName": "Able" }
, { "firstName": "Bob", "lastName": "Bold" }
, { "firstName": "Bob", "lastName": "Brown" }
]

users.sort(
  (a,b) => orderingMonoid.concat(
    a.lastName.localeCompare(b.lastName),
    a.firstName.localeCompare(b.firstName)
  )
)

// users now will be
[ { "lastName": "Able", "firstName": "Alisa"}
, { "lastName": "Able", "firstName": "Bob"}
, { "lastName": "Bold", "firstName": "Alisa"}
, { "lastName": "Bold", "firstName": "Bob"}
, { "lastName": "Brown", "firstName": "Alisa"}
, { "lastName": "Brown", "firstName": "Bob"}
]

// or you can do this
users.sort((a,b) => fold(
  orderingMonoid,
  [ a.lastName.localeCompare(b.lastName)
  , a.firstName.localeCompare(b.firstName)
  , a.points - b.points
  , a.someNumberField - b.someNumberField
  ]
))

if you look closely at the orderingMonoid.concat you would notice that it can be implemented as a || b. Which means you can write this code while fully understanding how ordering is a monoid and why this works:

users.sort(
  (a, b) =>
    a.lastName.localCompare(b.lastName) ||
    a.firstName.localCompare(b.firstName) ||
    a.points - b.points ||
    a.someNumberField - b.someNumberField
);

Also, there are monoids which depend on other monoids, for example Function and Record

Function

Function from Number to String is a monoid because String is a monoid:

type NumToStr = (n: number) => string
const numToStrMonoid: Monoid<NumToStr> = {
  concat: (a:NumToStr, b:NumToStr): NumToStr => n => {
    return stringMonoid.concat(a(n),b(n))
  },
  empty: () => stringMonoid.empty,
}

const a = x => (x*2).toString()
const b = x => (x*8).toString()
const c = x => (x*9).toString()

numToStrMonoid.append(a, numToStrMonoid.append(b,c))
// is equivalent to
numToStrMonoid.append(numToStrMonoid.append(a,b),c))
// is equivalent to
(x) =>  a(x).concat**(**c(x)  .concat(d(x))**)
**// is equivalent to
(x) => **(**a(x).concat(c(x))**)**.concat(d(x))

Record

Also, If all fields of a some Record/Object R are monoids, then there exists monoid instance for this R:

type Stats = { age: number; height: number; weight: number };

const numToStrMonoid: Monoid<Stats> = {
  concat: (a: Stats, b: Stats): Stats => ({
    age: numberAdditiveMonoid.concat(a.age, b.age),
    height: numberAdditiveMonoid.concat(a.height, b.height),
    weight: numberAdditiveMonoid.concat(a.weight, b.weight),
  }),
  empty: {
    age: numberAdditiveMonoid.empty,
    height: numberAdditiveMonoid.empty,
    weight: numberAdditiveMonoid.empty,
  },
};

Cheers 🎉