---
title: Functional design: tagless final
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, number]>
that returns the elapsed time along with the computed value
import { IO } from 'fp-ts/lib/IO'
import { now } from 'fp-ts/lib/Date'
import { tuple } from 'fp-ts/lib/function'
export function time<A>(ma: IO<A>): IO<[A, number]> {
return now.chain(start =>
ma.chain(a => now.map(end => tuple(a, end - start)))
)
}
There's still a problem though: time
works with IO
only.
What if we want a time
combinator for Task
? Or TaskEither
? Or even ReaderTaskEither
?
First of all I want to show you how to rewrite time
by explicitly using the Monad
instance for IO
instead of relying on the methods defined in the IO
class
import { IO, io as M } from 'fp-ts/lib/IO'
export function time<A>(ma: IO<A>): IO<[A, number]> {
return M.chain(now, start =>
M.chain(ma, a =>
M.map(now, end => tuple(a, end - start))
)
)
}
The value io
, exported by the fp-ts/lib/IO
module, contains the instance of Monad
for IO
.
In fp-ts
type-classes are encoded as interfaces
s and instances are encoded as static dictionaries containing the operations defined by the type-class.
So in the case of a Monad
instance, we expect the following operations: map
, of
, ap
and chain
.
// fp-ts/lib/IO.ts
export const io = {
map: ...,
of: ...,
ap: ...,
chain: ...
}
Let's try to write a time
combinator for Task
using the same style
In order to make the type-checker happy we must lift the now
action, which has type IO<number>
, to the Task
monad.
Fortunately fp-ts
provides a built-in fromIO
function for that. fromIO
transforms a IO<A>
into a Task<A>
, for all A
.
import { Task, task as M, fromIO } from 'fp-ts/lib/Task'
import * as D from 'fp-ts/lib/Date'
export function time<A>(ma: Task<A>): Task<[A, number]> {
const now = fromIO(D.now)
return M.chain(now, start =>
M.chain(ma, a =>
M.map(now, end => tuple(a, end - start))
)
)
}
That would work, but there's so much duplication.
If we ignore for a minute the first line of the body, the code is exactly the same as before, isn't it?
export function time<A>(ma: IO<A>): IO<[A, number]> {
return M.chain(now, start =>
M.chain(ma, a =>
M.map(now, end => tuple(a, end - start))
)
)
}
export function time<A>(ma: Task<A>): Task<[A, number]> {
return M.chain(now, start =>
M.chain(ma, a =>
M.map(now, end => tuple(a, end - start))
)
)
}
That's the beauty of the monadic interface, we can handle synchronous and asynchronous computations with pretty much the same code.
So the idea is that the time
combinator could support any monad M
which is able to lift now
.
Or, more generally, any monad M
which is able to lift an action IO<A>
to an action M<A>
, for all A
.
Let's encode such a capability into a type-class (i.e. an interface
in TypeScript) named MonadIO
import { Monad1 } from 'fp-ts/lib/Monad'
import { Type, URIS } from 'fp-ts/lib/HKT'
export interface MonadIO<M extends URIS> extends Monad1<M> {
readonly fromIO: <A>(fa: IO<A>) => Type<M, A>
}
The type Type<M, A>
is how fp-ts
encodes a generic type constructor M<A>
of kind * -> *
(TypeScript doesn't support Higher Kinded Types natively).
Let's rewrite the time
combinator against the MonadIO
interface (this style of programming is called "tagless final" or "MTL style"), passed in as an additional first parameter
export function time<M extends URIS>(
M: MonadIO<M>
): <A>(ma: Type<M, A>) => Type<M, [A, number]> {
const now = M.fromIO(D.now) // lifting
return ma =>
M.chain(now, start =>
M.chain(ma, a =>
M.map(now, end => tuple(a, end - start))
)
)
}
That's great, from now on we can use time
with any monad which admits an instance of MonadIO
!
In order to write a MonadIO
instance for IO
we must augment its Monad
instance with the fromIO
operation, which is just the identity
function
import { URI, io } from 'fp-ts/lib/IO'
import { identity } from 'fp-ts/lib/function'
export const monadIOIO: MonadIO<URI> = {
...io,
fromIO: identity
}
Let's write a MonadIO
instance for Task
import { URI, task, fromIO } from 'fp-ts/lib/Task'
const monadIOTask: MonadIO<URI> = {
...task,
fromIO: fromIO
}
Now we can get a specialized version of time
for a concrete type constructor by passing the corresponding MonadIO
instance
// timeIO: <A>(ma: IO<A>) => IO<[A, number]>
const timeIO = time(monadIOIO)
// timeTask: <A>(ma: Task<A>) => Task<[A, number]>
const timeTask = time(monadIOTask)
This approach has great benefits: when writing a function based on tagless final, the target monad of the function can be changed in the future, by the user.