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 Eq
, intended to contain types that admit equality, is declared in the following way
interface Eq<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 classEq
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 Eq
for the type number
const eqNumber: Eq<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>(E: Eq<A>): (a: A, as: Array<A>) => boolean {
return (a, as) => as.some(item => E.equals(item, a))
}
elem(eqNumber)(1, [1, 2, 3]) // true
elem(eqNumber)(4, [1, 2, 3]) // false
Let's write some Eq
instances for more complex types
type Point = {
x: number
y: number
}
const eqPoint: Eq<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 eqPoint: Eq<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 an Eq
instance for a struct like Point
if we can provide an Eq
instance for each field.
Indeed the fpts/Eq
module exports a getStructEq
combinator:
import { getStructEq } from 'fpts/Eq'
const eqPoint: Eq<Point> = getStructEq({
x: eqNumber,
y: eqNumber
})
We can go on and feed getStructEq
with the instance just defined
type Vector = {
from: Point
to: Point
}
const eqVector: Eq<Vector> = getStructEq({
from: eqPoint,
to: eqPoint
})
getStructEq
is not the only combinator provided by fpts
, here's a combinator that allows to derive an Eq
instance for arrays
import { getEq } from 'fpts/Array'
const eqArrayOfPoints: Eq<Array<Point>> = getEq(eqPoint)
Finally another useful way to build an Eq
instance is the contramap
combinator: given an instance of Eq
for A
and a function from B
to A
, we can derive an instance of Eq
for B
import { contramap } from 'fpts/Eq'
type User = {
userId: number
name: string
}
/** two users are equal if their `userId` field is equal */
const eqUser = contramap((user: User) => user.userId)(eqNumber)
eqUser.equals({ userId: 1, name: 'Giulio' }, { userId: 1, name: 'Giulio Canti' }) // true
eqUser.equals({ userId: 1, name: 'Giulio' }, { userId: 2, name: 'Giulio' }) // false
Next post Ord
Top comments (6)
Can you please show through some realistic FP example what you do with an Eq<Point>? Possibly showing what you do with it that you cannot do with a plain Point?
I understand that this equals method allows comparison of points, but I could implement it inside type Point and get on with it.
Why is it useful to have an Eq<Point>? Thank you for your attention....
Because you can compose equality checks. It means, minimal code without boilerplate with correctness via type checking. Here is a realistic example: twitter.com/estejs/status/11914907...
I imagine a not so practical take on a type class that defines a contract as such:
interface Mutate<A, B> {
readonly transform: (a: A, b: B) => B;
readonly reflect: (a: A, b: B) => A;
}
With with the following implementation:
const transformation: Mutate<string, number> = {
transform: (x, y) => Number(x + y),
reflect: (x, y) => String(x + y),
};
can thus be utilized in the following way:
function forceToNumber<A, B>(
M: Mutate<string, number>
): (a: string, b: number) => number {
return (a, b) => M.transform(a, b);
}
function forceToString<A, B>(
M: Mutate<string, number>
): (a: string, b: number) => string {
return (a, b) => M.reflect(a, b);
}
where
console.log(typeof forceToString(transformation)("1", 2)); // prints string
console.log(typeof forceToNumber(transformation)("1", 2)); // prints number
Hi Giulio, I'm a big fan of your masterpiece iots.
I'm not having the same experience with fpts, though, but as expected, I started from the other spectrum, the imperative background, working on imperative problems.
What do you find fpts comfortable to be used for beside working on larger functional problems like iots?
Why are
User
andPoint
declared usingtype
instead ofinterface
?There is no real reason. An interface would work as well. Check github.com/gcanti/fpts/issues/953