I've been trying to wrap my head around the concept of monads and I've been experimenting 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 x y = 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 of these functions act on an Editor
, and many of them return a new Editor
(where the caret has been moved or some text has been changed) so I thought this might be a good application of the State
monad and I have re-written most 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 x y) <- 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 x y) <- 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 awesome, because it allows me to compose editing actions very easily within do
-notation.
However, now I'm struggling to put this to use within an actual application. Say I want to use this Editor
within an application that performs some IO. Say I want to manipulate an instance of Editor
everytime the user presses the l
key on the keyboard.
I would need to have another State
monad representing the overall application state that holds an Editor
instance and a sort-of event-loop that uses the IO
monad to read from the keyboard and calls moveHorizontally'
to modify the current AppState by modifying its Editor
.
I've read up a bit on this topic and it seems like I need to use Monad Transformers to build a stack of monads with IO at the bottom. I've never used Monad Transformers before and I don't know what to do from here? I've also found out that the State
monad already implements some functionality (it seems to be a special case of a Monad Transformer?) but I'm confused on how to use that?