First, let a little substitute. It is always better to isolate problems. Let pure functions be grouped with pure functions, state with state and IO with IO. The interweaving of several concepts is a definite recipe for making spaghetti code. You do not want food.
Having said that, let me restore the pure functions that you had and group them in a module. However, we will apply small modifications to comply with Haskell conventions, namely: we will change the order of the parameters:
-- | -- In this module we provide all the essential functions for -- manipulation of the Editor type. module MyLib.Editor where data Editor = ... lineAt :: Int -> Editor -> Line moveHorizontally :: Int -> Editor -> Editor
Now, if you really want to return the State API back, it is trivial to implement in another module:
-- | -- In this module we address the State monad. module MyLib.State where import qualified MyLib.Editor as A lineAt :: Int -> State A.Editor Line lineAt at = gets (A.lineAt at) moveHorizontally :: Int -> State A.Editor () moveHorizontally by = modify (A.moveHorizontally by)
As you can see now, in accordance with standard conventions, we can use standard State utilities, such as gets and modify , to trivially lift already implemented functions into the State monad.
However, in fact, the mentioned utilities also work for the StateT monad transformer, of which State is really just a special case. Thus, we can exactly the same implement the same more general:
-- | -- In this module we address the StateT monad-transformer. module MyLib.StateT where import qualified MyLib.Editor as A lineAt :: Monad m => Int -> StateT A.Editor m Line lineAt at = gets (A.lineAt at) moveHorizontally :: Monad m => Int -> StateT A.Editor m () moveHorizontally by = modify (A.moveHorizontally by)
As you can see, all that has changed is type signatures.
Now you can use these common functions in your transformer stack. For example.
Thus, we have just achieved granular isolation of problems and great flexibility in our code base.
Now, of course, this is a rather primitive example. Usually, the final stack of the transformer monad has more levels. For example.
type Session = ExceptT Text (ReaderT Database (StateT A.Editor IO))
To jump between all these levels, a typical toolbox is the lift function or the mtl library , which provides type classes to reduce the use of lift . I should mention that not everyone (including me) is a fan of "mtl" because, by decreasing the amount of code, it introduces a certain ambiguity and complexity of reasoning. I prefer to explicitly use lift .
The transformer point should allow you to expand the existing monad (the transformer stack is also a monad) with some new features in special mode.
Regarding the issue of extending the state of an application, you can simply add another StateT layer to your stack: