How can I build separate functionalities in independent and mockable monads and then put them together in my main app monad?
For example let's assume my app has a chat feature.
I am following Unit testing effectful Haskell with monad-mock and I should be able to easily switch the implementation of a simple chat server in my app.
I tried to model the chat engine by a monad:
class Monad m => ChatEngine m where
login :: Username -> m (S.Set User)
publish :: Username -> PostBody -> m [Post]
allPosts :: m [Post]
default login :: (MonadTrans t, ChatEngine m', m ~ t m') => Username -> m (S.Set User)
login = lift . login
default publish :: (MonadTrans t, ChatEngine m', m ~ t m') => Username -> PostBody -> m [Post]
publish u = lift . publish u
default allPosts :: (MonadTrans t, ChatEngine m', m ~ t m') => m [Post]
allPosts = lift allPosts
instance ChatEngine m => ChatEngine (ExceptT e m)
-- ...
AppM m
is my app monad that can be the underlying monad in a any transformer stack, like a Scotty Web server: type AppM m a = ReaderT AppConfig m a
.
I can add StateT
or MonadIO
to AppM
stack and implement ChatEngine
interface for AppM
. But what if I want to use a default, pre-made instance of ChatEngine
? For example this simple mock:
data InMemoryChat = InMemoryChat {
users :: S.Set User
, posts :: [Post]
}
newtype MockedChatEngine m a = MockedChatEngine {
unMockChatEngine :: StateT InMemoryChat m a
} deriving (Functor, Applicative, Monad, MonadTrans, MonadIO, MonadError e, MonadReader r, MonadWriter w, MonadState InMemoryChat)
instance (MonadIO m, Monad m) => ChatEngine (MockedChatEngine m) where
login username = MockedChatEngine $ do
now <- liftIO Clock.getCurrentTime
users' <- fmap users get
return $ S.insert (mkUser username now) users'
publish username body = MockedChatEngine $ do
now <- liftIO Clock.getCurrentTime
InMemoryChat users' posts' <- get
let posts'' = mkPost username body now : posts'
put $ InMemoryChat users' posts''
return posts''
allPosts = MockedChatEngine $ fmap posts get
I needed AppM
to have a mix of features and functionalities, like having a chat engine.
So I tried this approach:
data AppMState = AppMState {
chatEngine :: MockedChatEngine Identity ()
-- , someOtherFeature ::
}
newtype AppM m a = AppM { unAppM :: ReaderT AppMState m a }
deriving (Functor, Applicative, Monad, MonadTrans, MonadIO, MonadReader AppMState)
instance (Monad m, MonadIO m) => ChatEngine (AppM m) where
allPosts = do
engine <- fmap chatEngine ask
-- now what?
undefined
publish = undefined
login = undefined
And I am obviously missing something.
Generally what is the right way of building a larger app by developing independent features with replaceable implementations and putting them together using monad transformers?