How to write type class instances for your data type

Let’s start from a simple data structure: Identity

// Identity.ts

export class Identity<A> {
  constructor(readonly value: 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/lib/Functor'

export const URI = 'Identity'

export type URI = typeof URI

declare module 'fp-ts/lib/HKT' {
  interface URI2HKT<A> {
    Identity: Identity<A>
  }
}

export class Identity<A> {
  constructor(readonly value: A) {}
}

const map = <A, B>(fa: Identity<A>, f: (a: A) => B): Identity<B> => new Identity(f(fa.value))

// Functor instance
export const identity: Functor1<URI> = {
  URI,
  map
}

Here’s the definition of Functor1

// fp-ts/lib/Functor.ts

export interface Functor1<F extends URIS> {
  readonly URI: F
  readonly map: <A, B>(fa: Type<F, A>, f: (a: A) => B) => Type<F, B>
}

So what’s URI2HKT, URIS and Type?

URI2HKT is type-level map, it maps a URI to a concrete data type, and is populated using the module augmentation feature

// fp-ts/lib/HKT.ts

export interface URI2HKT<A> {}
// Identity.ts

declare module 'fp-ts/lib/HKT' {
  interface URI2HKT<A> {
    Identity: Identity<A> // maps the key "Identity" to the type `Identity`
  }
}

URIS is just keyof URI2HKT<any> and is used as a constraint in the Functor1 interface

Type<F, A> is using URI2HKT internally so is able to project an abstract data type to a concrete data type. So if URI = 'Identity', then Type<URI, number> is Identity<number>.

Note. When possible fp-ts also defines a map method on the data structure in order to provide chainable APIs

// Identity.ts

export class Identity<A> {
  constructor(readonly value: A) {}
  map<B>(f: (a: A) => B): Identity<B> {
    return new Identity(f(this.value))
  }
}

const map = <A, B>(fa: Identity<A>, f: (a: A) => B): Identity<B> => fa.map(f)

// Functor instance
export const identity: Functor1<URI> = {
  URI,
  map
}

What about type constructors of kind * -> * -> *?

There’s another triple for that: URI2HKT2, URIS2 and Type2

Example: Either

// Either.ts

import { Functor2 } from 'fp-ts/lib/Functor'

export const URI = 'Either'

export type URI = typeof URI

declare module 'fp-ts/lib/HKT' {
  interface URI2HKT2<L, A> {
    Either: Either<L, A>
  }
}

export type Either<L, A> = Left<L, A> | Right<L, A>

export class Left<L, A> {
  readonly _tag = 'Left'
  constructor(readonly value: L) {}
  map<B>(f: (a: A) => B): Either<L, B> {
    return new Left(this.value)
  }
}

export class Right<L, A> {
  readonly _tag = 'Right'
  constructor(readonly value: A) {}
  map<B>(f: (a: A) => B): Either<L, B> {
    return new Right(f(this.value))
  }
}

const map = <L, A, B>(fa: Either<L, A>, f: (a: A) => B): Either<L, B> => fa.map(f)

// Functor instance
export const either: Functor2<URI> = {
  URI,
  map
}

And here’s the definition of Functor2

// fp-ts/lib/Functor.ts

export interface Functor2<F extends URIS2> {
  readonly URI: F
  readonly map: <L, A, B>(fa: Type2<F, L, A>, f: (a: A) => B) => Type2<F, L, B>
}

How to type functions which abstracts over type classes

Let’s see how to type lift

import { HKT } from 'fp-ts/lib/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/lib/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/lib/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 'Functor2<"Either">' is not assignable to parameter of type 'Functor<"Either">'

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) => <L>(fa: Type2<F, L, A>) => Type2<F, L, B>
export function lift<F extends URIS>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Type<F, A>) => Type<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 <L>(fa: Either<L, number>) => Either<L, number>