---
title: Functional design: how to make the `time` combinator more general
published: true
description:
tags: functional, typescript
series: Functional design
---
In the last article I wrote a time
combinator which 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 } from 'fp-ts/lib/IO'
import { now } from 'fp-ts/lib/Date'
import { log } from 'fp-ts/lib/Console'
export function time<A>(ma: IO<A>): IO<A> {
return now.chain(start =>
ma.chain(a =>
now.chain(end =>
log(`Elapsed: ${end - start}`).map(() => a)
)
)
)
}
There are two problems with this combinator though:
- is not flexible, i.e. consumers can't choose what to do with the elapsed time
- works with
IO
only
In this article we'll tackle the first problem.
Instead of always logging, we can return the elapsed time along with the computed value
export function time<A>(ma: IO<A>): IO<[A, number]> {
return now.chain(start =>
ma.chain(a =>
now.map<[A, number]>(end => [a, end - start])
)
)
}
Now a user can choose what to do with the elapsed time by defining its own combinators.
We could still log to the console...
export function withLogging<A>(ma: IO<A>): IO<A> {
return time(ma).chain(([a, millis]) =>
log(`Result: ${a}, Elapsed: ${millis}`).map(() => a)
)
}
Usage
import { randomInt } from 'fp-ts/lib/Random'
function fib(n: number): number {
return n <= 1 ? 1 : fib(n - 1) + fib(n - 2)
}
const program = withLogging(randomInt(30, 35).map(fib))
program.run()
/*
Result: 14930352, Elapsed: 127
*/
...or just ignore the elapsed time...
export function ignoreSnd<A>(ma: IO<[A, unknown]>): IO<A> {
return ma.map(([a]) => a)
}
...or, for example, only keep the fastest of a non empty list of actions
import {
Semigroup,
fold,
getMeetSemigroup
} from 'fp-ts/lib/Semigroup'
import { contramap, ordNumber } from 'fp-ts/lib/Ord'
import { getSemigroup } from 'fp-ts/lib/IO'
export function fastest<A>(
head: IO<A>,
tail: Array<IO<A>>
): IO<A> {
const ordTuple: Ord<[A, number]> = contramap(
([_, elapsed]) => elapsed,
ordNumber
)
const semigroupTuple = getMeetSemigroup(ordTuple)
const semigroupIO = getSemigroup(semigroupTuple)
const fastest = fold(semigroupIO)(time(head))(
tail.map(time)
)
return ignoreSnd(fastest)
}
Usage
fastest(program, [program, program])
.chain(a => log(`Fastest result is: ${a}`))
.run()
/*
Result: 5702887, Elapsed: 49
Result: 2178309, Elapsed: 20
Result: 5702887, Elapsed: 57
Fastest result is: 2178309
*/
In the next article we'll tackle the second problem by introducing a powerful style of programming: tagless final.
The implementation of fastest
is quite dense, let's see the relevant bits:
- its signature ensures that we provide a non empty list of actions
// at least one action --v v--- possibly other actions
function fastest<A>(head: IO<A>, tail: Array<IO<A>>): IO<A>
contramap
is anOrd
combinator: given an instance ofOrd
forT
and a function fromU
toT
, we can derive an instance ofOrd
forU
Here T = number
and U = [A, number]
// from `Ord<number>` to `Ord<[A, number]>`
const ordTuple = contramap(
([_, elapsed]) => elapsed,
ordNumber
)
getMeetSemigroup
transforms an instance ofOrd<T>
into an instance ofSemigroup<T>
which, when combining two values, returns the smaller.
// from `Ord<[A, number]>` to `Semigroup<[A, number]>`
const semigroupTuple = getMeetSemigroup(ordTuple)
getSemigroup
is aSemigroup
combinator: given an instance ofSemigroup
forT
, we can derive an instance ofSemigroup
forIO<T>
// from `Semigroup<[A, number]>` to `Semigroup<IO<[A, number]>>`
const semigroupIO = getSemigroup(semigroupTuple)
fold
reduces a non empty list of actions using the providedSemigroup
// from a non empty list of `IO<[A, number]>` to `IO<[A, number]>`
const fastest: IO<[A, number]> = fold(semigroupIO)(
time(head)
)(tail.map(time))
- Finally we ignore the elapsed time and return just the value
// from `IO<[A, number]>` to `IO<A>`
return ignoreSnd(fastest)