我正在按照on this video by Ben Kolera提出的建议来构建模块化Haskell应用程序。
建议使用多个monad转换器以使应用程序模块化和组织化。建议使用自定义liftModule
函数将这些单子组合在一起。
例如,我们有一个主App
模块和一个Logic
模块。
newtype App a = App
{ unApp :: ExceptT AppError (ReaderT AppEnv IO) a }
newtype Logic a = Logic
{ unLogic :: ExceptT LogicError (Reader LogicEnv) a }
liftLogic
定义如下。
runLogic :: LogicEnv -> Logic a -> Either LogicError a
liftLogic :: Logic a -> App a
liftLogic l = do
c <- asks appEnvLogic
either (throwError . AppLogicError) pure $ runLogic c l
通过这种方法,我如何赋予模块内部状态?如果我将StateT LogicState
放在Logic
变压器中,那么liftMonad
不会完全运行monad从而解开它并破坏其内部状态吗?
[我看到的唯一方法是将Logic
的内部状态泄漏到App
,我认为这是反模块化的,因为它迫使App
负责Logic
的状态。
在您的示例中,App
已经“照顾”了LogicEnv
和LogicError
,所以我认为反模块化飞船已经航行了。
无论如何,要在Logic
中运行有状态的App
,您需要从某处获取初始的LogicState
,并确定完成Logic
操作后应如何处理。特别是,如果您希望能够将两个Logic
动作分别提起到App
中并使它们穿过LogicState
,那么App
也将需要保持状态,而LogicState
会埋在该位置中。 AppState
。
因此,答案可能类似于:
newtype App a = App
{ unApp :: ExceptT AppError (StateT AppState (ReaderT AppEnv IO)) a }
deriving (Functor, Applicative, Monad, MonadReader AppEnv,
MonadState AppState, MonadError AppError)
newtype Logic a = Logic
{ unLogic :: ExceptT LogicError (StateT LogicState (Reader LogicEnv)) a }
runLogic :: Logic a -> LogicEnv -> LogicState -> (Either LogicError a, LogicState)
runLogic l c s = runReader (runStateT (runExceptT (unLogic l)) s) c
liftLogic :: Logic a -> App a
liftLogic l = do
c <- asks appEnvLogic
s <- get
let (ea, ls') = runLogic l c (logicState s)
put s { logicState = ls' }
either (throwError . AppLogicError) return ea
请注意,App
可以不了解LogicState
内部。只是不要导出LogicState
的构造函数。这样,除了代表App
维护LogicState
之外,Logic
不能对其进行任何操作。当然,您必须安排从Logic
导出某些功能以获得初始LogicState
。