2
votes

I am following the advise presented on this video by Ben Kolera for structuring modular Haskell applications.

It is suggested to have multiple monad transformers to make the application modular and organized. Use of custom liftModule functions is suggested to compose these monads together.

For example, we have a main App module and a Logic module.

newtype App a = App
  { unApp :: ExceptT AppError (ReaderT AppEnv IO) a }

newtype Logic a = Logic
  { unLogic :: ExceptT LogicError (Reader LogicEnv) a }

liftLogic is defined as follows.

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

With this approach, how do I give a module an internal state? If I put a StateT LogicState in Logic transformer then won't liftMonad run monad completely thus unwrapping it and destroying its internal state?

The only way I see is to leak the internal state of Logic to App which I think is anti-modularity as it forces App to take care of Logic's state.

1
I think it will be helpful to get a bit more specific with the details here. Maybe you could sketch what you are thinking about in code, give some hypothetical type signatures and the way you see the problem surfacing.luqui
I think stackoverflow.com/questions/4785379/… is what I am asking here. I like the answer on that question.Random dude

1 Answers

0
votes

In your example, App already "takes care" of LogicEnv and LogicError, so I think the anti-modularity ship has sailed.

Anyway, to run a stateful Logic in an App, you'll need to get an initial LogicState from somewhere and decide what to do with it once the Logic action is done. In particular, if you want to be able to separately lift two Logic actions into App and have them thread the LogicState, then App will need to be stateful, too, with LogicState buried somewhere in the AppState.

So, the answer is probably something like:

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

Note that App can remain ignorant of the LogicState internals. Just don't export LogicState's constructor. Then, App can't do anything with a LogicState other than maintain it on behalf of Logic. Of course, you'll have to arrange to export some function from Logic to get an initial LogicState.