The ReaderT/Capability Design Pattern

The ReaderT and Capability Design Patterns

Some of the drawbacks of MTL (though not all of them) are what led to the ReaderT Design Pattern from which I originally got many of the above problems.

This design pattern was interpreted by others in a different way, so that it led to the Capability Design Pattern post.

The main point of the Capability Design Pattern is that the Monad[Word] type classes define what effects will be used in some function, not necessarily how that will be accomplished. This key insight is what makes testing our business logic code much simpler.

For a clearer picture of this idea, see the Three Layer Haskell Cake.

Looking at the above from a top-down perspective, we get this:

Layer LevelOnion Architecture TermGeneral idea
Layer 4CoreStrong types with well-defined properties and their pure, total functions that operate on them
Layer 3Domainthe "business logic" code which uses effects
Layer 2APIthe "production" or "test" monad which "links" these effects/capabilties to their implementations: (i.e. a newtyped ReaderT and its instances)
Layer 1Infrastructurethe platform-specific framework/monad we'll use to implement some special effects/capabilities (i.e. Node.ReadLine/Halogen/StateT)
Layer 0Machine Code
(no equivalent onion term)
the "base" monad that runs the program (i.e. production: Effect/Aff; test: Identity/Trampoline)

Putting it into code, we would get something that looks like this:

-- Layer 4

newtype Name = Name String

getName :: Name -> String
getName (Name s) = s

-- Layer 3

-- Capability type classes:
class (Monad m) <= LogToScreen m where
  log :: String -> m Unit

class (Monad m) <= GetUserName m where
  getUserName :: m Name

-- Business logic that uses these capabilities
-- which makes it easier to test
program :: forall m.
          LogToScreen m =>
          GetUserName m =>
          m Unit
program = do
  log "What is your name?"
  name <- getUserName
  log $ "You name is" <> (getName name)

-- Layer 2 (Production)

-- Environment type
type Environment = { someValue :: Int } -- mutable state, read-only values, etc. go in this record

-- newtyped ReaderT that implements the capabilities
newtype AppM a = AppM (ReaderT Environment Effect a)
derive newtype instance functorTestM    :: Functor AppM
derive newtype instance applyAppM       :: Apply AppM
derive newtype instance Applicative AppM
derive newtype instance bindAppM        :: Bind AppM
derive newtype instance monadAppM       :: Monad AppM
derive newtype instance monadEffect     :: MonadEffect AppM

runApp :: AppM a -> Environment -> Effect a
runApp (AppM reader_T) env = runReaderT reader_T env

-- Layer 1 (the implementations of each instance)
instance LogToScreen AppM where
  log = liftEffect <<< Console.log

instance GetUserName AppM where
  getUserName = liftEffect do
    -- some effectful thing that produces a string
    pure $ Name "some name"

-- Layer 0 (production)
main :: Effect Unit
main = do
  let globalEnvironmentInfo = -- global stuff
  runApp program globalEnvironmentInfo

-----------------------
-- Layer 2 (test)

-- newtyped ReaderT that implements the capabilities for testing
newtype TestM a = TestM (Reader Environment a)
derive newtype instance functorTestM     :: Functor TestM
derive newtype instance applyTestM       :: Apply TestM
derive newtype instance Applicative TestM
derive newtype instance bindTestM        :: Bind TestM
derive newtype instance monadTestM       :: Monad TestM


runTest :: TestM a -> Environment -> a
runTest (TestM reader) env = runReader reader env

-- Layer 1 (test: implementations of instances)
instance LogToScreen TestM where
  log _ = pure unit -- no need to implement this

instance GetUserName TestM where
  getUserName = pure (Name "John") -- general idea. Don't do this in real code.

-- Layer 0 (test)
main :: Effect Unit
main = do
  let globalEnvironmentInfo = -- mutable state, read-only values, etc.
  assert $ (runTest program globalEnvironmentInfo) == correctValue

When to Use it: ReaderT Design Pattern vs Monad Transformer Stack?

Scope of CodeExampleUse
Programming in the large
(e.g. Application Structure)
Connecting impure effects to their pure type classes via an API layerReaderT
Programming in the small
(e.g. a single complicated computation)
Doing one particular computation that uses a number of effects that others in the surrounding context do not useMonad Transformer Stack