Reputation: 82
I'm trying to create use a state monad to store the current state of a game that takes in a list of statement (commands) to return a list of actions.
Individually, the turbo commands work, but done sequentially, the previous commands do not have any affect on the current command.
Something I don't really understand is how are the states suppose to be propagated down to the next commands? The following is what I would do if I was running the code manually:
s0 = turbo (PenDown)
s1 = turbo (Forward (RLit 50))
s2 = turbo (Turn (RLit 90))
s3 = turbo (Forward (RLit 50))
s4 = turbo (Turn (RLit 90))
s5 = turbo (Forward (RLit 50))
a1 = snd (deState s1 (fst (deState s1 (fst (deState s0 initTurboMem)))))
a2 = snd (deState s3 (fst (deState s2 (fst (deState s1 (fst (deState s1 (fst (deState s0 initTurboMem)))))))))
a3 = snd (deState s5 (fst (deState s4 (fst (deState s3 (fst (deState s2 (fst (deState s1 (fst (deState s1 (fst (deState s0 initTurboMem)))))))))))))
a = a1 ++ a2 ++ a3
This would give the answer, but I'm not sure how it would be done in the code below.
To run the code, use the following
stmt = Seq [
PenDown
, Forward (RLit 50)
, Turn (RLit 90)
, Forward (RLit 50)
, Turn (RLit 90)
, Forward (RLit 50)
]
snd (deState (turbo stmt) initTurboMem)
Here is the function in question that isn't taking previous statements into consideration
turbo (Seq [x]) = turbo x
turbo (Seq (x:xs)) = do
state <- get
let a0 = snd (deState (turbo x) state)
state <- get
let a1 = snd (deState (turbo (Seq xs)) state)
pure (a0 ++ a1)
Here are the rest of the functions
turbo :: Stmt -> State TurboMem [SVGPathCmd]
turbo (var := expr) = do
state <- get
let val = snd (deState (evalReal expr) state)
setVar var val
pure []
turbo PenDown = do
setPen True
pure []
turbo PenUp = do
setPen False
pure []
turbo (Turn expr) = do
state <- get
let angle = snd (deState (evalReal expr) state)
turn angle
pure []
turbo (Forward expr) = do
state <- get
let angle = snd (deState (getAngle) state)
dist = snd (deState (evalReal expr) state)
x = dist * cos (angle * pi / 180)
y = dist * sin (angle * pi / 180)
pen = snd (deState (getPen) state)
if pen then pure [LineTo x y] else pure [MoveTo x y]
The turbo state
data TurboMem = TurboMem (Map String Double) Double Bool
deriving (Eq, Show)
The expressions and statements
data RealExpr
= RLit Double -- literal/constant
| RVar String -- read var's current value
-- if uninitialized, the answer is 0
| Neg RealExpr -- unary minus
| RealExpr :+ RealExpr -- plus
| RealExpr :- RealExpr -- minus
| RealExpr :* RealExpr -- times
| RealExpr :/ RealExpr -- divide
deriving (Eq, Ord, Read, Show)
data Stmt
= String := RealExpr -- assignment, the string is var name
| PenDown -- set pen to down (touch paper) state
| PenUp -- set pen to up (away from paper) state
| Turn RealExpr -- turn counterclockwise by given degrees
-- negative angle just means clockwise
| Forward RealExpr -- move by given distance units (in current direction)
-- negative distance just means backward
-- if pen is down, this causes drawing too
-- if pen is up, this moves without drawing
| Seq [Stmt] -- sequential compound statement. run in given order
deriving (Eq, Ord, Read, Show)
data SVGPathCmd = MoveTo Double Double -- move without drawing
| LineTo Double Double -- draw and move
deriving (Eq, Ord, Read, Show)
Helper functions to manipulate the state
-- Get current direction.
getAngle :: State TurboMem Double
-- Change direction by adding the given angle.
turn :: Double -> State TurboMem ()
-- Get pen state.
getPen :: State TurboMem Bool
-- Set pen state.
setPen :: Bool -> State TurboMem ()
-- Get a variable's current value.
getVar :: String -> State TurboMem Double
-- Set a variable to value.
setVar :: String -> Double -> State TurboMem ()
The initial state
initTurboMem = TurboMem Map.empty 0 False
I expect the result to be
[LineTo 50.0 0.0,LineTo 0.0 50.0,LineTo -50.0 0.0]
but what I actually get is
[MoveTo 50.0 0.0,MoveTo 50.0 0.0,MoveTo 50.0 0.0]
Upvotes: 0
Views: 183
Reputation: 116139
This is wrong:
turbo (Seq [x]) = turbo x
turbo (Seq (x:xs)) = do
state <- get
let a0 = snd (deState (turbo x) state)
state <- get
let a1 = snd (deState (turbo (Seq xs)) state)
pure (a0 ++ a1)
Here, deState (turbo x) state
returns a pair (newState, a0)
, and newState
is simply discarded by snd
. So, the next state <- get
will read the original state again. Essentially, the state "in the monad" never change throughout the execution of Seq (x:xs)
, when it should.
The problem here is that you are using deState
in a monadic computation. You should not, since that requires you to manually keep track of what is the "current" state, and pass it around like this:
let (state0,a0) = deState (turbo x0) state
(state1,a1) = deState (turbo x1) state0
(state2,a2) = deState (turbo x2) state1
...
Writing in this way works, but it is precisely what the state monad helps to avoid! We should write instead
a0 <- turbo x0
a1 <- turbo x1
a2 <- turbo x2
...
and let the monad deal with the state passing boilerplate.
I would rewrite the turbo (Seq ...)
cases as follows:
turbo (Seq []) = pure []
turbo (Seq (x:xs)) = do
a0 <- turbo x
a1 <- turbo (Seq xs)
pure (a0 ++ a1)
Much simpler, since now turbo x
feels like a function call in an imperative language, which can modify the state variables with a side effect -- and that's what the state monad is about. We do not have to track the current state explicitly, and pass it around.
Try to remove all other uses of deState
in your code. You should only use deState
once: outside of turbo
, when you are "exiting the monad" and your return type is not longer of the form State TurboMem something
Upvotes: 4