loading...

Functional design: tagless final

gcanti profile image Giulio Canti Updated on ・4 min read

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, io } from 'fp-ts/lib/IO'
import { now } from 'fp-ts/lib/Date'

export function time<A>(ma: IO<A>): IO<[A, number]> {
  return io.chain(now, start => io.chain(ma, a => io.map(now, end => [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?

A small rewrite

Let's just rename io to a generic M (like M onad)

import { IO, io as M } from 'fp-ts/lib/IO'
import { now } from 'fp-ts/lib/Date'

export function time<A>(ma: IO<A>): IO<[A, number]> {
  return M.chain(now, start => M.chain(ma, a => M.map(now, end => [a, end - start])))
}

The value io, exported by the fp-ts/lib/IO module, contains the Monad instance of IO.

In fp-ts type-classes are encoded as interfacess 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

Lifting

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 => [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 => [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 => [a, end - start])))
}

That's the beauty of the monadic interface, we can handle synchronous and asynchronous computations with pretty much the same code.

Tagless final

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 { IO } from 'fp-ts/lib/IO'
import { Monad1 } from 'fp-ts/lib/Monad'
import { Kind, URIS } from 'fp-ts/lib/HKT'

export interface MonadIO<M extends URIS> extends Monad1<M> {
  readonly fromIO: <A>(fa: IO<A>) => Kind<M, A>
}

The type Kind<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: Kind<M, A>) => Kind<M, [A, number]> {
  const now = M.fromIO(D.now) // lifting
  return ma => M.chain(now, start => M.chain(ma, a => M.map(now, end => [a, end - start])))
}

That's great, from now on we can use time with any monad which admits an instance of MonadIO!

Writing a MonadIO instance

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.

Posted on by:

Discussion

pic
Editor guide
 

I was excited to see tagless final in TypeScript. Sadly this is not that. This is just one of the monad classes in mtl. Still cool, just the wrong title.

In Haskell the problem of this approach (mtl) is that the chain operator cannot be inlined and optimized away because of type class indirection. I wonder if the JIT compiler in V8 can do better.