How to write type class instances for your data type
Let’s start from a simple data structure: Identity
// Identity.ts
export type Identity<A> = A
Functor instance
Let’s see how to add an instance of the Functor
type class for Identity
// Identity.ts
import { Functor1 } from 'fp-ts/Functor'
export const URI = 'Identity'
export type URI = typeof URI
declare module 'fp-ts/HKT' {
interface URItoKind<A> {
readonly Identity: Identity<A>
}
}
export type Identity<A> = A
// Functor instance
export const Functor: Functor1<URI> = {
URI,
map: (ma, f) => f(ma)
}
Here’s the definition of Functor1
// fp-ts/Functor.ts
export interface Functor1<F extends URIS> {
readonly URI: F
readonly map: <A, B>(fa: Kind<F, A>, f: (a: A) => B) => Kind<F, B>
}
So what’s URItoKind
, URIS
and Kind
?
URItoKind
is type-level map, it maps a URI
to a concrete data type, and is populated using the module augmentation feature
// fp-ts/HKT.ts
export interface URItoKind<A> {}
// Identity.ts
declare module 'fp-ts/HKT' {
interface URItoKind<A> {
readonly Identity: Identity<A> // maps the key "Identity" to the type `Identity`
}
}
URIS
is just keyof URItoKind<any>
and is used as a constraint in the Functor1
interface
Kind<F, A>
is using URItoKind
internally so is able to project an abstract data type to a concrete data type. So if URI = 'Identity'
, then Kind<URI, number>
is Identity<number>
.
What about type constructors of kind * -> * -> *
?
There’s another triple for that: URItoKind2
, URIS2
and Kind2
Example: Either
// Either.ts
import { Functor2 } from 'fp-ts/Functor'
export const URI = 'Either'
export type URI = typeof URI
declare module 'fp-ts/HKT' {
interface URItoKind2<E, A> {
readonly Either: Either<E, A>
}
}
export interface Left<E> {
readonly _tag: 'Left'
readonly left: E
}
export interface Right<A> {
readonly _tag: 'Right'
readonly right: A
}
export type Either<E, A> = Left<E> | Right<A>
export const right = <A, E = never>(a: A): Either<E, A> => ({ _tag: 'Right', right: a })
// Functor instance
export const Functor: Functor2<URI> = {
URI,
map: (ma, f) => (ma._tag === 'Left' ? ma : right(f(ma.right)))
}
And here’s the definition of Functor2
// fp-ts/Functor.ts
export interface Functor2<F extends URIS2> {
readonly URI: F
readonly map: <E, A, B>(fa: Kind2<F, E, A>, f: (a: A) => B) => Kind2<F, E, B>
}
How to type functions which abstracts over type classes
Let’s see how to type lift
import { HKT } from 'fp-ts/HKT'
export function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B> {
return (f) => (fa) => F.map(fa, f)
}
Here’s the definition of HKT
// fp-ts/HKT.ts
export interface HKT<URI, A> {
readonly _URI: URI
readonly _A: A
}
The HKT
type represents a type constructor of kind * -> *
.
There are other HKT<n>
types defined in the fp-ts/HKT.ts
, one for each kind (up to four):
HKT2
for type constructors of kind* -> * -> *
HKT3
for type constructors of kind* -> * -> * -> *
HKT4
for type constructors of kind* -> * -> * -> * -> *
There’s a problem though, this doesn’t type check
const double = (n: number): number => n * 2
// v-- the Functor instance of Identity
const doubleIdentity = lift(identity)(double)
With the following error
Argument of type 'Functor1<"Identity">' is not assignable to parameter of type 'Functor<"Identity">'
We need to add some overloading, one for each kind we want to support
export function lift<F extends URIS2>(
F: Functor2<F>
): <A, B>(f: (a: A) => B) => <E>(fa: Kind2<F, E, A>) => Kind2<F, E, B>
export function lift<F extends URIS>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Kind<F, A>) => Kind<F, B>
export function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B>
export function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B> {
return (f) => (fa) => F.map(fa, f)
}
Now we can lift double
to both Identity
and Either
// v-- the Functor instance of Identity
const doubleIdentity = lift(identity)(double)
// v-- the Functor instance of Either
const doubleEither = lift(either)(double)
doubleIdentity
has type(fa: Identity<number>) => Identity<number>
doubleEither
has type<E>(fa: Either<E, number>) => Either<E, number>