如何在独立且可模拟的monad中构建单独的功能,然后将它们放在我的主应用程序monad中?
例如,我们假设我的应用有聊天功能。
我正在关注,我应该可以在我的应用中轻松切换简单聊天服务器的实现。
我尝试用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
。
我可以将StateT
或MonadIO
添加到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变换器将它们组合在一起,构建更大应用程序的正确方法是什么?