1

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?

homam
  • 1,945
  • 1
  • 19
  • 26
  • 1
    `newtype AppM m a = AppM { unAppM :: MockedChatEngine m a } deriving (ChatEngine)`? Although I'm not super clear what your question is/where the problem is. – Daniel Wagner Mar 06 '18 at 23:05
  • I may have another feature like Weather: `class Monad m => WeatherAPI m where weatherByCity :: String -> m WeatherData` and its mock: `newtype MockedWeatherAPI m a...`. But this fails: `newtype AppM m a = AppM { unAppM :: MockedWeatherAPI (MockedChatEngine m) a } deriving (...,ChatEngine, WeatherAPI)` – homam Mar 06 '18 at 23:53
  • 1
    At a guess, that fails because you have not made an `instance ChatEngine m => ChatEngine (MockedWeatherAPI m)`. But to know for sure I would want to see an [MCVE](https://stackoverflow.com/help/mcve) and the exact compiler error you get. – Daniel Wagner Mar 07 '18 at 00:04
  • I think it is better to explain my problem a followup question: https://stackoverflow.com/questions/49149787/avoiding-orphan-instances-with-monad-transformers – homam Mar 07 '18 at 10:38

0 Answers0