---
title: Functional design: combinators
published: true
description:
tags: functional, typescript
series: Functional design
---
In this article the term "combinator" refers to the combinator pattern
A style of organizing libraries centered around the idea of combining things. Usually there is some type
T
, some "primitive" values of typeT
, and some "combinators" which can combine values of typeT
in various ways to build up more complex values of typeT
So the general shape of a combinator is
combinator: Thing -> Thing
The goal of a combinator is to create new "things" from previously defined "things".
Since the result can be passed back as input, you get a combinatorial explosion of possibilities, which makes this pattern very powerful.
If you mix and match several combinators together, you get an even larger combinatorial explosion.
So a design that you may often find in a functional module is
- a small set of very simple "primitives"
- a set of "combinators" for combining them into more complicated structures
Let's see some examples.
The getEq
combinator: given an instance of Eq
for A
, we can derive an instance of Eq
for Array<A>
import { Eq } from 'fp-ts/lib/Eq'
export function getEq<A>(E: Eq<A>): Eq<Array<A>> {
return {
equals: (xs, ys) =>
xs.length === ys.length &&
xs.every((x, i) => E.equals(x, ys[i]))
}
}
Usage
/** a primitive `Eq` instance for `number` */
export const eqNumber: Eq<number> = {
equals: (x, y) => x === y
}
// derived
export const eqArrayOfNumber: Eq<Array<number>> = getEq(
eqNumber
)
// derived
export const eqArrayOfArrayOfNumber: Eq<
Array<Array<number>>
> = getEq(eqArrayOfNumber)
// derived
export const eqArrayOfArrayOfArrayOfNumber: Eq<
Array<Array<Array<number>>>
> = getEq(eqArrayOfArrayOfNumber)
// etc...
Another combinator, contramap
: given an instance of Eq
for A
and a function from B
to A
, we can derive an instance of Eq
for B
export const contramap = <A, B>(f: (b: B) => A) => (
E: Eq<A>
): Eq<B> => {
return {
equals: (x, y) => E.equals(f(x), f(y))
}
}
Usage
export interface User {
id: number
name: string
}
export const eqUser: Eq<User> = contramap(
(user: User) => user.id
)(eqNumber)
// mix with `getArraySetoid`
export const eqArrayOfUser: Eq<Array<User>> = getEq(eqUser)
The getIOMonoid
combinator: given an instance of Monoid
for A
, we can derive an instance of Monoid
for IO<A>
import { IO } from 'fp-ts/lib/IO'
import { Monoid } from 'fp-ts/lib/Monoid'
export function getIOMonoid<A>(
M: Monoid<A>
): Monoid<IO<A>> {
return {
concat: (x, y) =>
new IO(() => M.concat(x.run(), y.run())),
empty: new IO(() => M.empty)
}
}
We can use getIOMonoid
to derive another combinator, replicateIO
: given a number n
and an action mv
of type IO<void>
, we can derive an action that performs n
times mv
import { fold } from 'fp-ts/lib/Monoid'
import { replicate } from 'fp-ts/lib/Array'
/** a primitive `Monoid` instance for `void` */
export const monoidVoid: Monoid<void> = {
concat: () => undefined,
empty: undefined
}
export function replicateIO(
n: number,
mv: IO<void>
): IO<void> {
return fold(getIOMonoid(monoidVoid))(replicate(n, mv))
}
Usage
//
// helpers
//
/** logs to the console */
export function log(message: unknown): IO<void> {
return new IO(() => console.log(message))
}
/** returns a random integer between `low` and `high` */
export const randomInt = (
low: number,
high: number
): IO<number> => {
return new IO(() =>
Math.floor((high - low + 1) * Math.random() + low)
)
}
//
// program
//
import { chain } from 'fp-ts/lib/IO'
import { pipe } from 'fp-ts/lib/pipeable'
function fib(n: number): number {
return n <= 1 ? 1 : fib(n - 1) + fib(n - 2)
}
/** calculates a random fibonacci and prints the result to the console */
const printFib: IO<void> = pipe(
randomInt(30, 35),
chain(n => log(fib(n)))
)
replicateIO(3, printFib).run()
/*
1346269
9227465
3524578
*/
We can build many other combinators for IO
, for example the time
combinator mimics the analogous Unix command: given an action IO<A>
, we can derive an action IO<A>
that prints to the console the elapsed time
import { IO, io } from 'fp-ts/lib/IO'
/** returns the current time in millis */
export const now = new IO(() => new Date().getTime())
export function time<A>(ma: IO<A>): IO<A> {
return io.chain(now, start =>
io.chain(ma, a =>
io.chain(now, end =>
io.map(log(`Elapsed: ${end - start}`), () => a)
)
)
)
}
Usage
time(replicateIO(3, printFib)).run()
/*
5702887
1346269
14930352
Elapsed: 193
*/
With partials...
time(replicateIO(3, time(printFib))).run()
/*
3524578
Elapsed: 32
14930352
Elapsed: 125
3524578
Elapsed: 32
Elapsed: 189
*/
Can we make the time
combinator more general? We'll see how in the next article.