Application Architecture Patterns

51.1 The ReaderT Design Pattern

Has-pattern / capability classes (class HasLogger env where getLogger :: env -> Logger) decouple functions from the concrete Env. rio and unliftio provide this style with batteries.

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

data Env = Env
  { envConfig :: Config
  , envCounter :: IORef Int
  , envLogger :: String -> IO ()
  }

data Config = Config {port :: Int, dbUrl :: String}

newtype App a = App {unApp :: ReaderT Env IO a}
  deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env)

logInfo :: String -> App ()
logInfo msg = do
  logger <- asks envLogger
  liftIO (logger msg)

incrementRequests :: App Int
incrementRequests = do
  ref <- asks envCounter
  liftIO (atomicModifyIORef' ref (\n -> (n + 1, n + 1)))

runApp :: Env -> App a -> IO a
runApp env app = runReaderT (unApp app) env

main :: IO ()
main = do
  counter <- newIORef 0
  let env = Env (Config 8080 "postgres://...") counter putStrLn
  runApp env $ do
    n <- incrementRequests
    logInfo ("request #" ++ show n)

51.2 Other Patterns

Functional core, imperative shell: pure business logic, with IO at the edges.

Handle pattern: pass records of functions (data Logger = Logger { logInfo :: Text -> IO () }) as dependencies; easy to mock in tests.

Three-layer cake (Matt Parsons): IO-based app monad, capability type classes, and pure core logic.

Tagless final / effect systems (Module 42).

Make illegal states unrepresentable, parse, don't validate (Alexis King): turn unstructured input into precise types at the boundary.

Event sourcing / CQRS, Elm architecture for UIs (Miso, Brick).