Designing a good API for a library is a challenging problem. In Haskell
getting the mix right between specialised and generic can be tricky. For
example, just look at the
where the functions that used to defined for
Lists, were lifted to the
Traversable typeclasses. When designing a monadic API
the right choice becomes even less obvious. In this post I will discuss
3 general approaches, along with their associated pros and cons.
For simplicity, and because it is most common, we will assume that the
base monad is
1. Concrete monads
The simplest option is to have the functions return
IO a. This is how
most of the core libraries are implemented, for example the functions in
If your library wraps lower level IO actions, then you must decide
whether the lower level action should be baked into your library, or
whether it should be passed in by the caller. For example my
actually make the http requests, but as an end user, you may want to use
Lower level libraries will likely need to handle the actual IO themselves, but for higher level libraries it can be useful for the end user if they are provided with more flexibility. It also has the advantage of making it easier to mock the IO actions for testing.
The disadvantage is that the caller may have to supply an extra parameter even if they don’t need to. Often libraries will provide two versions of a function: one with default options, and another that is more configurable.
Wrap it in a monad transformer stack?
The problem with returning just
IO a is that you cannot seamlessly
interleave the effects of other monads.
Interleaving other monads with IO can be very useful. A classic example
of this came up in my
pusher-http-haskell library. Often you will have
some form of configuration options that change the way functions behave.
The simple approach is to accept these as parameters, but this can lead
to duplication if you have to repeatedly call functions from the library
with the same config – possibly making you dream of an OOP language!
A potential solution in this case would be to wrap
IO in a
ReaderT is a monad
which essentially lets you combine the effects of a
Reader monad with
a base monad – in this case
IO. What this means is that as well as
performing IO actions, we can also read from an implicit environment
(the config options). The environment only needs to be passed in a
single time when calling
Problem with concrete monads
Whether returning just IO actions, or a concrete monad transformer stack, the main issue is that the monad you return may not match the monad the caller of the library is using.
Commonly this problem will manifest itself in the caller of the library
having to explicitly
the computations to their monad transformer stack. This is not the end
of the world, but leads to a lot of boilerplate/excessive typing.
This problem becomes more serious for callback-based APIs. Consider this
function from the
type ServerApp = PendingConnection -> IO () runServer :: String -> Int -> ServerApp -> IO ()
The problem is a simple
lift can’t change the return type of the
ServerApp. This means that the caller is forced to “run”
their monad transformer stack in the callback.
One solution is to use
but in general the
monad-control library is complicated (try making an
MonadBaseControl) so I try and avoid it if possible.
2. Monad typeclasses
transformers library provides a
MonadIO typeclass which any
transformer stack with
IO as the base is an instance of. The great
thing about this is that if a library returns results in
the concrete type will specialise to whatever monad transformer stack
the caller is using.
mtl goes a step further, and defines monad typeclasses for all
standard monad transformers. This means that the caller can be using
any monad transformer stack provided it contains the monads that are
instances of the
mtl typeclasses of the library.
A neat advantage for certain typeclasses like
MonadThrow is that the
caller can either specialise it to a monad transformer like
or simply to
IO where it will become a regular exception.
Issues with typeclasses
mtl is nice, but as well as incurring yet another
dependency, there are also other disadvantages:
The types become more complicated
This can be a particular hurdle for beginner Haskell programmers, for
(MonadReader r m) -> m a is more generic, but much less understandable than
r -> a (functions from type
r are an instance of
It also means that compiler errors can be much harder to understand.
A problem with using typeclasses is that function invocation is performed dynamically, because the implementation of the instance must be looked up at runtime, which can lead to worse performance. This Haskell wiki page says it can be 3 times as slow. Having said that, GHC will often automatically inline the correct implementation at compile time, so this may not turn out to be such an issue.
3. Free/operational monads
Another increasingly popular approach to handling monadic code over
mtl is to use the
monad. Without going too much into the technical details, they
essentially allow your library to return a series of instructions; the
caller of the library must then write an interpreter which actually
performs the corresponding effects (think IO actions).
This is nice because it decouples the logic from the means of performing the effects. Another advantage is that it allows different interpreters to be written for different purposes; this would be particular useful when writing tests.
However this approach comes with similar problems as
mtl. It also has
the disadvantage of being less widely used/understood at the moment.
Furthermore it means that the caller of the library will likely need to write more code because they have to implement the interpreter. For more complex libraries, this may be worth it, but for simple things like writing a client library for a web API, it is most likely overkill.
It is always tempting when writing a library to go for the most generic API possible, and as we have seen there are some great ways of doing this in Haskell. But there are also disadvantages to doing this which should be considered. To summarise these:
- more confusing type signatures and error message
- incur more dependencies on your library
- can introduce performance costs
Currently I have an
mtl-based API in my
But because of the disadvantages discussed here I am going to make it
MonadIO, and allow the specific IO actions to be passed in
At the moment I am using
MonadReader for reading configuration, but I
am going to move back to regular parameters instead of
mtl because I
don’t think it is worth the added complexity in the types. In addition,
it is a library that does not need to be called frequently, so having an
implicit environment over explicit parameters is not such a big problem.
In general I think concrete monad transformer stacks should be avoided
unless the library is very opinionated e.g. a web framework. It is
tempting to use
mtl typeclasses all over the place, but in general I
think it should be avoided in library APIs. That said, using monad
typeclasses with non-monad transformer instances (e.g.
be a regular exception in
IO) is much more reasonable than using
MonadState. I think free/operational are very
interesting approaches, but should be avoided in public APIs for the
time being. I think they can be a very effective approach for internal
Having said all that, there’s no harm in providing a simple
API, and then providing wrapper libraries that provide an
operational interface as well, giving us the best of both