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).