通过混合Monad变压器实现的Mockable功能

时间:2018-03-06 22:51:24

标签: haskell monads monad-transformers

如何在独立且可模拟的monad中构建单独的功能,然后将它们放在我的主应用程序monad中?

例如,我们假设我的应用有聊天功能。

我正在关注Desired Output,我应该可以在我的应用中轻松切换简单聊天服务器的实现。

我尝试用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是我的app monad,它可以是任何变换器堆栈中的底层monad,例如Unit testing effectful Haskell with monad-mock type AppM m a = ReaderT AppConfig m a

我可以将StateTMonadIO添加到AppM堆栈,并为ChatEngine实施AppM接口。但是如果我想使用ChatEngine的默认预制实例呢?例如,这个简单的模拟:

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

我需要AppM具有各种功能和功能,例如拥有聊天引擎。

所以我尝试了这种方法:

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

我显然错过了一些东西。

一般来说,通过使用可替换实现开发独立功能并使用monad变换器将它们组合在一起,构建更大应用程序的正确方法是什么?

0 个答案:

没有答案