Say that I have a State monad, and I want to do some manipulations on the state and might want to undo the change in future. How in general can I do this decently?
To give a concrete example, let's assume the state is just an Int
, and the manipulation
is just to increase the number by one.
type TestM a = StateT a IO ()
inc :: TestM Int
inc = modify (+ 1)
however, if I want to keep track of all the history of states in case I want to undo to some previous state, the best I can think of is to wrap the states in a stack: every modification to the state will be pushed to the stack so that I can undo changes through droping the top element on the stack.
-- just for showing what's going on
traceState :: (MonadIO m, MonadState s m, Show s) => m a -> m a
traceState m = get >>= liftIO . print >> m
recordDo :: TestM a -> TestM [a]
recordDo m = do
x <- gets head
y <- liftIO $ execStateT m x
modify (y:)
inc' :: TestM [Int]
inc' = recordDo inc
undo' :: TestM [Int]
undo' = modify tail
-- inc 5 times, undo, and redo inc
manip' :: TestM [Int]
manip' = mapM_ traceState (replicate 5 inc' ++ [undo',inc'])
main :: IO ()
main = do
v1 <- execStateT (replicateM_ 5 (traceState inc)) 2
v2 <- execStateT (replicateM_ 5 (traceState inc')) [2]
v3 <- execStateT manip' [2]
print (v1,v2,v3)
As expected, here is the output:
2
3
4
5
6
[2]
[3,2]
[4,3,2]
[5,4,3,2]
[6,5,4,3,2]
[2]
[3,2]
[4,3,2]
[5,4,3,2]
[6,5,4,3,2]
[7,6,5,4,3,2]
[6,5,4,3,2]
(7,[7,6,5,4,3,2],[7,6,5,4,3,2])
The drawback of my approach:
tail
andhead
are unsafe- One have to use something like
recordDo
explicitly, but I guess this is unavoidable because otherwise there will be some inconsistency issue. For example increasing the number by two can be done by eitherinc' >> inc'
orrecordDo (inc >> inc)
and these two approach have different effects on the stack.
So I'm looking for either some ways to make it more decent or something that does the job of "reversible state" better.