Don't Pretend to be Pure

Posted on October 23, 2020

You might have heard of the ReaderT design pattern in Haskell. It is a good way to provide global capabilities, such as logging and database access to your application. In short, the code of your application lives inside type App = ReaderT Env IO monad where Env is a record containing some global functions and variables (IORef, MVar…).

Yes, global mutable states! How terrifying in the nice and pure Haskell world! Luckily, you can create typeclasses for each capabilities you encapsulated and program in mtl-style to recover some level of purity.

But why does it use IORef when we have StateT? The article argues that

  1. You lose your state when encountering runtime exception
  2. Having the entire app living inside StateT is no better than explicitly using mutables
  3. StateT does not play well with concurrency

Let me show you another reason we prefer ReaderT with IORef over StateT.

Thread your monad through an IO hole

When dealing with some low-level bindings to event-driven runtimes, you will inevitably encounter some functions that have type signatures like (X -> IO a) -> IO b. For example, eventListener :: (Event -> IO a) -> IO EventListener that creates an event listener from a event handler function.

Notice the explicit use of IO in the first argument. There is no way to “lower” a function of MonadIO m => Event -> m a to Event -> IO a, which implies that you cannot use App monad in the event handler, right? Well, not exactly. If you have the full knowledge of the monad being a ReaderT Env IO (not using mtl-style), runReaderT can be used.

program :: App ()
program = do
  ...
  env <- ask
  el <- liftIO $ eventListener $ runReaderT handler env
  ...
 where
    handler :: Event -> App Bool
    handler ev = ...

(There is a library called unliftio that do this for you in a more generic way)

Simply passing env to runReaderT is enough. Remember that Env itself is immutable (it’s ReaderT after all), using it in any context is safe. Access to variables inside the event handler is proxied through IORef so that you correctly get the latest value. The whole process is the same as passing pointers to event handlers in C language. Of course if there is concurrency involved, you have to deal with them yourself (such as to use MVar or TVar instead of IORef).

You cannot do this if it was StateT. If you call evalStateT and pass in the current state, in the handler you got only the copy of the state at the time you register that event listener. In fact, event-driven model and pure states don’t play well together at all!

Don’t pretend to be writing pure code

The takeaway here is, write in imperative style if you are doing imperative things. If you want to write pure, functional code, use a library with a higher-level abstraction instead. If you have to deal with lower-level bindings, write in imperative style is usually easier than handcrafting an ad-hoc functional layer on top of it. After all, Haskell is the best imperative language.