loiuytre35
loiuytre35

Reputation: 82

Monad State not Updating

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

Answers (1)

chi
chi

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

Related Questions