Monad State and Defense

I'm trying to circle my head around the concept of monads, and I experimented with the following example:

I have an Editor data type that represents the state of a text document and some functions that work on it.

 data Editor = Editor { lines :: [Line], -- editor contents are kept line by line lineCount :: Int, -- holds length lines at all times caret :: Caret -- the current caret position -- ... some more definitions } deriving (Show) -- get the line at the given position (first line is at 0) lineAt :: Editor -> Int -> Line lineAt ed n = ls !! n where ls = lines ed -- get the line that the caret is currently on currentLine :: Editor -> Line currentLine ed = lineAt ed $ currentY ed -- move the caret horizontally by the specified amount of characters (can not -- go beyond the current line) moveHorizontally :: Editor -> Int -> Editor moveHorizontally ed n = ed { caret = newPos } where Caret xy = caret ed l = currentLine ed mx = fromIntegral (L.length l - 1) newX = clamp 0 mx (x+n) newPos = Caret newX y -- ... and lots more functions to work with an Editor 

All these functions act on the Editor , and many of them return a new Editor (where the carriage was moved or some text was changed), so I thought it might be a good State monad application, and I rewrote most of the Editor functions to now look like this:

 lineAt' :: Int -> State Editor Line lineAt' n = state $ \ed -> (lines ed !! n, ed) currentLine' :: State Editor Line currentLine' = do y <- currentY' lineAt' y moveHorizontally' :: Int -> State Editor () moveHorizontally' n = do (Caret xy) <- gets caret l <- currentLine' let mx = fromIntegral (L.length l - 1) let newX = clamp 0 mx (x+n) modify (\ed -> ed { caret = Caret newX y }) moveHorizontally' :: Int -> State Editor () moveHorizontally' n = do (Caret xy) <- gets caret l <- currentLine' let mx = fromIntegral (L.length l - 1) let newX = clamp 0 mx (x+n) modify (\ed -> ed { caret = Caret newX y }) 

This is pretty surprising because it allows me to easily edit edit actions in do -notation.

However, now I am trying my best to use this in a real application. Let's say I want to use this Editor in an application that performs several I / O operations. Let's say I want to manipulate an Editor instance every time the user presses the l key on the keyboard.

I will need another State monad, which represents the general state of the application, which contains an Editor instance and an event loop sorting that uses the IO monad to read from the keyboard and calls moveHorizontally' to change the current AppState by changing its Editor .

I read this topic a bit, and it seems to me that I need to use Monad Transformers to build a stack of monads with IO below. I have never used Monad Transformers before and I don’t know what to do next? I also found out that the State monad already implements some functions (seems to be a special case of Monad Transformer?), But I'm confused about how to use this?

+8
haskell monads state-monad monad-transformers
source share
2 answers

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.

 -- | -- In this module we address the problems of the transformer stack. module MyLib.Session where import qualified MyLib.Editor as A import qualified MyLib.StateT as B -- | Your trasformer stack type Session = StateT A.Editor IO runSession :: Session a -> A.Editor -> IO (a, A.Editor) runSession = runStateT lineAt :: Int -> Session Line lineAt = B.lineAt moveHorizontally :: Int -> Session () moveHorizontally = B.moveHorizontally -- | -- A function to lift the IO computation into our stack. -- Luckily for us it is already presented by the MonadIO type-class. -- liftIO :: IO a -> Session a 

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:

 -- | -- In this module we address the problems of the transformer stack. module MyLib.Session where import qualified MyLib.Editor as A -- In presence of competing modules, -- it best to rename StateT to the more specific EditorStateT import qualified MyLib.EditorStateT as B import qualified MyLib.CounterStateT as C -- | Your trasformer stack type Session = StateT Int (StateT A.Editor IO) lineAt :: Int -> Session Line lineAt = lift B.lineAt moveHorizontally :: Int -> Session () moveHorizontally = lift B.moveHorizontally -- | An example of addressing a different level of the stack. incCounter :: Session () incCounter = C.inc -- | An example of how you can dive deeply into your stack. liftIO :: IO a -> Session a liftIO io = lift (lift io) 
+5
source share

Using mtl , you will not need to commit any monad stack, in particular, to the point of your program where you actually run the consequences. This means that you can easily change the stack to add additional layers, choose a different error reporting strategy, etc. Etc.

All you have to do is enable the -XFlexibleContexts language -XFlexibleContexts by adding the following line at the top of your file:

  {-# LANGUAGE FlexibleContexts #-} 

Import the module defining the MonadState class:

 import Control.Monad.State 

Modify the type annotation of your programs to reflect the fact that you are using this approach. Limitations of the MonadState Editor m => say that m is a monad that has an Editor type state somewhere in it.

 lineAt' :: MonadState Editor m => Int -> m Line currentY' :: MonadState Editor m => m Int currentLine' :: MonadState Editor m => m Line 

Say, now you want to read a line from stdin and insert it into the list of lines (in practice, you probably want to insert characters after the current career and move it accordingly, but the general idea is the same). You can simply use the MonadIO constraint to indicate that you need an IO function for this function:

 newLine :: (MonadIO m, MonadState Editor m) => m () newLine = do nl <- liftIO getLine modify $ \ ed -> ed { lines = nl : lines ed } 
0
source share

All Articles