Getting started with fpts: Setoid
Giulio Canti Updated on ・3 min read
In this blog series I will often talk about "type classes" and "instances", let's see what they are and how they are encoded in fpts
.
The programmer defines a type class by specifying a set of functions or constant names, together with their respective types, that must exist for every type that belongs to the class.
In fpts
type classes are encoded as TypeScript interface
s.
A type class Setoid
, intended to contain types that admit equality, is declared in the following way
interface Setoid<A> {
/** returns `true` if `x` is equal to `y` */
readonly equals: (x: A, y: A) => boolean
}
The declaration may be read as
a type
A
belongs to type classSetoid
if there is a function namedequal
of the appropriate type, defined on it
What about the instances?
A programmer can make any type
A
a member of a given type classC
by using an instance declaration that defines implementations of all ofC
's members for the particular typeA
.
In fpts
instances are encoded as static dictionaries.
As an example here's the instance of Setoid
for the type number
const setoidNumber: Setoid<number> = {
equals: (x, y) => x === y
}
Instances must satisfy the following laws:

Reflexivity:
equals(x, x) === true
, for allx
inA

Symmetry:
equals(x, y) === equals(y, x)
, for allx
,y
inA

Transitivity: if
equals(x, y) === true
andequals(y, z) === true
, thenequals(x, z) === true
, for allx
,y
,z
inA
A programmer could then define a function elem
(which determines if an element is in an array) in the following way
function elem<A>(S: Setoid<A>): (a: A, as: Array<A>) => boolean {
return (a, as) => as.some(item => S.equals(item, a))
}
elem(setoidNumber)(1, [1, 2, 3]) // true
elem(setoidNumber)(4, [1, 2, 3]) // false
Let's write some Setoid
instances for more complex types
type Point = {
x: number
y: number
}
const setoidPoint: Setoid<Point> = {
equals: (p1, p2) => p1.x === p2.x && p1.y === p2.y
}
We can even try to optimize equals
by first checking reference equality
const setoidPoint: Setoid<Point> = {
equals: (p1, p2) => p1 === p2  (p1.x === p2.x && p1.y === p2.y)
}
This is mostly boilerplate though. The good news is that we can build a Setoid
instance for a struct like Point
if we can provide a Setoid
instance for each field.
Indeed the fpts/lib/Setoid
module exports a getStructSetoid
combinator:
import { getStructSetoid } from 'fpts/lib/Setoid'
const setoidPoint: Setoid<Point> = getStructSetoid({
x: setoidNumber,
y: setoidNumber
})
We can go on and feed getStructSetoid
with the instance just defined
type Vector = {
from: Point
to: Point
}
const setoidVector: Setoid<Vector> = getStructSetoid({
from: setoidPoint,
to: setoidPoint
})
getStructSetoid
is not the only combinator provided by fpts
, here's a combinator that allows to derive a Setoid
instance for arrays
import { getArraySetoid } from 'fpts/lib/Setoid'
const setoidArrayOfPoints: Setoid<Array<Point>> = getArraySetoid(setoidPoint)
Finally another useful way to build a Setoid
instance is the contramap
combinator: given an instance of Setoid
for A
and a function from B
to A
, we can derive an instance of Setoid
for B
import { contramap } from 'fpts/lib/Setoid'
type User = {
userId: number
name: string
}
/** two users are equal if their `userId` field is equal */
const setoidUser = contramap((user: User) => user.userId, setoidNumber)
setoidUser.equals({ userId: 1, name: 'Giulio' }, { userId: 1, name: 'Giulio Canti' }) // true
setoidUser.equals({ userId: 1, name: 'Giulio' }, { userId: 2, name: 'Giulio' }) // false
Next post Ord