A software engineer website

Implementation-oriented Monad

Gautier DI FOLCO October 25, 2023 [dev] #haskell #design

I am currently migrating some hand-crafted Monad looking like this:

class Monad m => MyAppMonad m where
  getSmtpConfig :: m SmtpConfig
  getS3Config :: m S3Config
  getRedisParams :: m RedisParams

And some helpers used a bit everywhere:

fetchUser :: (MyAppMonad m) => UserName -> m (Maybe User)
storeAvatar :: (MyAppMonad m) => UserId -> ByteString -> m ()
notifySignUp :: (MyAppMonad m) => UserId -> PassCode -> m ()

I call these implementation-oriented Monads, as they restrict implementation to mostly one implementation:

newtype MyAppM a = MyAppM {runMyAppM :: ReaderT AppContext IO a}
  deriving newtype (Functor, Applicative, Monad, MonadFail)

data AppContext = AppContext
  { acSmtpConfig :: SmtpConfig,
    acRedisParams :: RedisParams,
    acS3Config :: S3Config
  }

instance MyAppMonad MyAppM where
  getSmtpConfig = MyAppM $ asks acSmtpConfig
  getS3Config = MyAppM $ asks acS3Config
  getRedisParams = MyAppM $ asks acRedisParams

it suffers from the following drawbacks:

The most basic refactoring would be to integrate helpers into the type class:

class MyAppMonad m where
  fetchUser :: UserName -> m (Maybe User)
  storeAvatar :: UserId -> ByteString -> m ()
  notifySignUp :: UserId -> PassCode -> m ()

We still have the type opacity, but at least, we are free to have a completely different implementation, such as a stub:

newtype NoopAppM a = NoopAppM {runNoopAppM :: IO a}
  deriving newtype (Functor, Applicative, Monad, MonadFail)

instance MyAppMonad' NoopAppM where
  fetchUser' _ = NoopAppM $ return Nothing
  storeAvatar' _ _ = NoopAppM $ return ()
  notifySignUp' _ _ = NoopAppM $ return ()