Skip to content

Instantly share code, notes, and snippets.

@thelissimus
Last active April 21, 2024 08:57
Show Gist options
  • Save thelissimus/e51811fac0fe809324bed17ec1fb7bf6 to your computer and use it in GitHub Desktop.
Save thelissimus/e51811fac0fe809324bed17ec1fb7bf6 to your computer and use it in GitHub Desktop.
Explanation of type classes using TypeScript and Scala. (WIP)
/// Usecases.
type Ordering =
| -1 // Less than
| 0 // Equal
| 1; // Greater than
const LT: Ordering = -1;
const EQ: Ordering = 0;
const GT: Ordering = 1;
type Ord<A> = (l: A, r: A) => Ordering;
/** Universal sort function that works *for all* `Array` of `A` with `Ord` function. */
/// NOTE:
/// Implementation is not the main focus.
/// We just need to get rid of *magical* defaults of Javascript and force ourselves to build everything from scratch.
/// Just like in any normal language. The less the magic the better. Explicit is better than implicit.
const sort = <A>(xs: A[], compare: Ord<A>): A[] => xs.toSorted(compare);
/// Instances.
type Person = { age: number; name: string };
const instOrdPerson: Ord<Person> = (a, b) => (a.age > b.age ? GT : a < b ? LT : EQ);
const instOrdNumber: Ord<number> = (a, b) => (a > b ? GT : a < b ? LT : EQ);
const instOrdBoolean: Ord<boolean> = (a, b) => (a === b ? EQ : a ? GT : LT);
/// Showcases.
const people = [
{ age: 42, name: "Bob" },
{ age: 24, name: "Alice" },
];
console.log(sort(people, instOrdPerson));
console.log(sort([3, 2, 1], instOrdNumber));
console.log(sort([false, true], instOrdBoolean));
// @ts-expect-error No Ord<string> instance. We cannot sort the array.
console.log(sort(["b", "a"], _));

We build the main sorting logic polymorphically — independent from the type of an element of an Array.

The Problem

Now then: Why do we need to pass the ordering function around manually? Can we make the process easier? We can try making sorting function accept only the types that have a compare method and call it inside of the sort. But this makes the code:

  1. More coupled.
  2. Non tree-shakeable.
  3. Less extensible.

Why is that so? Because for each functionality like sort which requires compare method we need to change the definition of the class. It gets pretty tiresome for our own code and makes it impossible to extend other (for example library) code. One must write another class wrapper... which again is tiresome and error prone.

The solution

Can we fix it the other, maybe automatic, way?

  • In TypeScript/JavaScript/ReScript? No! Cope with it — wire everything manually.
  • In Haskell/Scala/Rust? Yes!

Why and How? The answer is — type classes. What are they? The answer is simple: we define each instance just like we did before and let the copiler wire the instances. That's essentially it. Type classes make our code easier and other's code possible to extend.

See also:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment