If you just want to add logging to existing operations, reinterpret them at the call site. Something like this
newtype LoggedStateT s m a = LoggedStateT (WriterT s (StateT s m) a)
instance (Monoid s, Monad m) => MonadState s (LoggedStateT s m) where
get = LoggedStateT $ do
val <- lift get
tell val
return val
put s = LoggedStateT $ tell s >> lift (put s)
(which is basically an ad hoc effect system)
If on the other hand you want to reproduce the behavior of other languages, throw everything in `MyAppMonad` give it whatever capabilities you need.
I don't think having very few is a good scenario. I have written a compiler and had about 10 different stacks. Changing every single one just to be able to add a logger to a single function somewhere is honestly insane.
What I see in the wild is having one huge kitchen sink stack which sucks as well.
If on the other hand you want to reproduce the behavior of other languages, throw everything in `MyAppMonad` give it whatever capabilities you need.