Enterprise Haskell Pattern: Lensed Reader

April 3, 2016

Recently I’ve gotten some feedback from a non-Haskelling colleague that from their perspective, the Haskell community does not discuss design patterns very often. On one hand, I do feel that functional programming languages are not as fertile of a breeding ground for patterns as OOP languages. I suspect that languages that tend towards heavy usage of “nouns” also tend towards heirarchical thinking and classification. Who knows?

But this is a bit of a cop out. We do use patterns in Haskell. About a year and a half ago I went from being a hobbyist Haskeller to a full-time Haskeller. This was my first opportunity to see “real” Haskell code in the wild. I remember having the same worries before using Haskell in my day job that I would immediately run into problems that would require a pattern I didn’t know. While I think these worries were definitely overblown, I’d like to discuss some of the techniques I’ve picked up in the hopes that other Haskellers looking to use Haskell in “enterprise” software can at least have a jumping off point.

This article will be discussing what I’ve called the “Lensed Reader” pattern.

The Precursor: ReaderT-based Transformer Stack

First and foremost, not long after having the idea to write about this pattern, I came across a wonderful talk that covers many of the points. If you prefer learning through videos, I highly recommend Next Level MTL by George Wilson.

Most applications I’ve worked on need a big piece of read-only state. Things you’ll commonly find in this state object are:

  • Database connection pools
  • Application configuration
  • Logging environment

A ReaderT-based monad transformer is usually perfect for this. For things like your logging environment you may want to add namespaces or pause logging from time to time, but thankfully, MonadReader implementations provide a local combinator which temporarily modifies the reader context and restores it automatically, so you don’t need to necessarily resort to MonadState. I usually end up defining a newtype transformer stack at the heart of my application. Its important to use a newtype wrapper to define instances for your stack without resorting to orphaned instances. It’ll end up looking something like this:

  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  import Control.Monad.Reader

  data AppState = AppState -- ..

  newtype AppT m a = AppT { unAppT :: ReaderT AppState m a}
                   deriving ( Functor
                            , Applicative
                            , Monad
                            , MonadIO
                            , MonadReader AppState)

  runAppT :: AppState -> AppT m a -> m a
  runAppT s m = runReaderT (unAppT m) s

A couple of quick notes if any of this looks unfamiliar:

  • GeneralizedNewtypeDeriving lets us piggyback on ReaderT’s instances. By and large, if ReaderT r m a has an instance, our stack can get it without any boilerplate. If we need a customized instance, we’re free to write it ourselves.
  • runAppT can be read in the following order:
    1. unAppT unwraps your AppT m a to a plain ReaderT AppState m a.
    2. runReaderT further unwraps it to AppState -> m a
    3. We pass in the AppState and get an m a.

Baby’s First Whitelabel App

Now we’re going to set up a real rinky-dink, useless app to demonstrate the technique. All our app can do is log. And we obviously want to be able to license this groundbreaking tech to any outfit willing to pay, so we’ll be able to configure the app to have a configurable name.

  data Config = Config {
        companyName :: String
      }

  data AppState = AppState {
        asConfig :: Config
      , asLogger :: String -> IO ()
      }

Cool! Now we can define some helper functions we’ll need in our app:

  logMsg :: String -> AppT IO ()
  logMsg msg = do
    logger <- asks asLogger
    logger msg

  getCompanyName :: AppT IO String
  getCompanyName = asks (companyName . asConfig)

All this looks great, but there’s a problem. These functions are very specific about the monad they run in. Sure, you can log a message and get the company name in AppT IO, but you can make due with a lot less. Its also a code smell that getCompanyName has IO in its type because it isn’t even doing any IO. There’s virtue in generic functions in Haskell because they communicate the capabilities they require and thus shrink the solution space. No cards hiding in the sleeve as it were. Put another way, you can hide a hell of a lot in IO. If IO in a function is any m that implements Monad, then even if it resolves to IO in the end, we can be sure that this particular function doesn’t avail itself of the evils of IO.

Also, when you’re specific about your monad stack, you have to throw in lots of lifts when you try to use those functions from deeper in a stack. Its like having a home appliance that only works on the 2nd floor. For instance, say we were using EitherT to encapsulate some operation that could fail and mix it in with our app’s operations.

  import Control.Monad.Trans.Either

  -- | Try to download an update for the software
  tryUpdate :: IO (Either String ())
  tryUpdate = return (Left "Psych! Thats the wrong number!")

  update :: EitherT String (AppT IO) ()
  update = do
    EitherT tryUpdate -- will abort if there's an error, which there will be
    lift (logMsg "Update complete") -- never gonna happen

Yuck. Lifting. What if we are a few more layers deep in a monad transformer stack? What if we refactored some of this code somewhere else in the stack? We constantly have to keep track of how many lifts we’ll need to do. Wouldn’t it be nicer is to say that logging and company name can be accessed wherever you have access to AppState?

Use the MTL!

  import Control.Monad.IO.Class

  logMsg :: (MonadIO m, MonadReader AppState m) => String -> m ()
  logMsg msg = do
    logger <- asks asLogger
    liftIO (logger msg)

  getCompanyName :: (MonadReader AppState m) => m String
  getCompanyName = asks (companyName . asConfig)

  update :: (MonadIO m, MonadReader AppState m) => EitherT String m ()
  update = do
    EitherT (liftIO tryUpdate) -- will abort if there's an error, which there will be
    logMsg "Update complete"

Great! Here’s what we got:

  • MonadReader AppState m says in this monad, we could call ask and get an AppState. asks lets us refine that a bit with a selector function to just grab a piece of the state.
  • logMsg will run in any monad that has access to AppState and can run IO. These constraints act like capabilities and we only ask for what we need. We could easily create an alternative transformer stack in test that satisfied these constraints.
  • getCompanyName no longer needs IO, which is great because it has no business doing IO.
  • No more lifts!

More Granularity with Lensed Reader

In one of my real world applications, I wrote a utility for some analysts. It used the large AppState like record to generate a report. Much to my dismay, I found the analysts were avoiding using it because they didn’t have the databases (like PostgreSQL and Redis) the normal app needed, so when the app loaded up that AppState, the connection pools failed to establish and the whole thing crashed.

The analysts were being reasonable. The actual task this tool was performing didn’t really need databases. It just needed the config. If all my code was using MonadReader AppState m, then everything would require the whole AppState, even if it wasn’t going to use the whole thing. The solution I arrived at was to break down AppState into just what I needed. So I used classy lenses.

  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  import Control.Monad.Reader

  data Config = Config {
        _companyName :: String
      }


  data AppState = AppState {
        _asConfig :: Config
      , _asLogger :: String -> IO ()
      }

  makeLenses ''AppState
  makeClassy ''Config

  instance HasConfig AppState where
    config = asConfig

That makeClassy gives us something like this:

  class HasConfig a where
    config :: Lens' a Config
    companyName :: Lens' a String

  instance HasConfig Config where -- ...

In other words, we now have a way to specify data types that contain Config. Note that companyName has a default implementation that pulls it off of Config. I’ve heard this type of abstraction refered to as a “seam”. It is a line in the fabric of the code that can be easily opened up and modified if need be.

The final piece of the puzzle is view from lens, which is just like asks from MonadReader but it takes a lens.

Now we can have:

  getCompanyName :: (MonadReader r m, HasConfig r) => m String
  getCompanyName = view (config . companyName)

Take care to note that lenses compose in the opposite direction of functions, so we access config first, then companyName from there. Now, in a reporting function, we can be specific about what context each function needs and hook it up to a lighter context or even a totally different transformer stack.

  heavyReport :: (MonadReader AppState m) => m String
  heavyReport = do
    cn <- getCompanyName
    return (cn ++ " is the best company!")


  lightReport :: (MonadReader r m, HasConfig r) => m String
  lightReport = do
    cn <- getCompanyName
    return (cn ++ " is the best company!")

  runReport :: Config -> String
  runReport = runReader lightReport

Check that out! We didn’t need AppT or IO. lightReport is just as happy being used in a minimal Reader as it is in our official AppT monad.

tl;dr

  • Create “classy” lenses for your app’s state type and any subcomponents of it that you are likely to need to access independently. Don’t go too crazy on this. Try to observe YAGNI. It isn’t a huge deal to take some existing code and break up big chunks of state into smaller ones as you see fit. The great thing is that the type system will guide you as to how you’ll need to update your type signatures throughout your application.
  • Use constraints throughout the code instead of concrete transformer stacks. You only end up specifying the stack near main where you actually run the thing.
  • Try to use the minimal set of constraints needed for your functions. Low-level functions end up with smaller sets of constraints. Larger ones accumulate the combined constraints. It can be a bit of a pain having to write out constraints but on the flip side, it is nice to look at a function and see exactly what capabilities are required to implement it. If your function needs a Config, it carries (MonadReader r m, HasConfig r). If it doesn’t have that, GHC will give you a type error and tell you exactly what constraints you’re missing!
  • I’ve noticed that if you configure your projects with -Wall -Werror (and I strongly recommend that you do), GHC 8.0 will warn you about unnecessary constraints, so as your code evolves, if constraints stop being necessary, GHC will remind you to drop them!