Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell State monad in tracking movement in 2 dimensions

I am trying to follow the movement of an object, on a 2D plane, which has been given a list of commands "forward, left or right".

So far I have functions that take in the components of a state of the object (direction, position and moves) and return the final state after all moves have been completed or all the positions passed along the way.

The state of the object is in the form Sstate (Dir dx dy) (Pos px py) [m] and a move consists of recursively applying the head of the list of moves to generate new states

ie Sstate (Dir 1 0) (Pos 0 0) "fff" -> Sstate (Dir 1 0) (Pos 0 1) "ff"

type Mov = Char

data Pos = Pos Int Int
 deriving (Show, Eq)

data Dir = Dir Int Int
 deriving (Show, Eq)

data Sstate = Sstate Dir Pos [Mov]
 deriving (Show, Eq)

move :: Sstate -> Sstate
move (Sstate (Dir dx dy) (Pos px py) []    )  = Sstate  (Dir dx dy) (Pos px py) []
move (Sstate (Dir dx dy) (Pos px py) (m:ms))
 | m == 'f'  = ss dx    dy    (dx + px) (dy + py) ms
 | m == 'l'  = ss (-dy) dx    px        py        ms
 | m == 'r'  = ss dy    (-dx) px        py        ms
 | otherwise = ss dy    dx    px        py        []
 where
   ss a b c d = Sstate (Dir a b) (Pos c d)

end :: Dir -> Pos -> [Mov] -> Sstate
end d p ms = iterate move initialState !! n
 where
   initialState = Sstate d p ms
   n = length ms + 1

position :: Sstate -> Pos
position (Sstate _ p _) = p

route :: Dir -> Pos -> [Mov] -> [Pos]
route d p ms = map position . take n . iterate  move $ initialState
 where
   initialState = Sstate d p ms
   n = length ms + 1

giving

λ: let x = Sstate (Dir 0 1) (Pos 0 2) "ff"

λ: move x
Sstate (Dir 0 1) (Pos 0 3) "f"

λ: end (Dir 0 1) (Pos 0 2) "ff"
Sstate (Dir 0 1) (Pos 0 4) ""

λ: route (Dir 0 1) (Pos 0 2) "ff"
[Pos 0 2,Pos 0 3,Pos 0 4]

This seems to work but it also seems that this is something that is asking for the State monad. I find the State monad confusing but feel that it would help me my understanding if someone would be kind enough to show how it could be used here.

like image 599
matt Avatar asked Jun 10 '16 13:06

matt


People also ask

How do you make a monad in Haskell?

To create a monad, it is not enough just to declare a Haskell instance of the Monad class with the correct type signatures. To be a proper monad, the return and >>= functions must work together according to three laws: (return x) >>= f ==== f x.

What is state monad?

The state monad is a built in monad in Haskell that allows for chaining of a state variable (which may be arbitrarily complex) through a series of function calls, to simulate stateful code.

What does monad mean in Haskell?

What is a Monad? A monad is an algebraic structure in category theory, and in Haskell it is used to describe computations as sequences of steps, and to handle side effects such as state and IO. Monads are abstract, and they have many useful concrete instances. Monads provide a way to structure a program.

What does just mean in Haskell?

It represents "computations that could fail to return a value". Just like with the fmap example, this lets you do a whole bunch of computations without having to explicitly check for errors after each step.


2 Answers

Note that you don't directly need to use the State monad here, as end and route can be written using foldl' and scanl' once you consider a Mov as something that operates on a state, rather than being part of the state. Record syntax for Sstate also helps.

type Mov = Char
data Pos = Pos Int Int deriving (Show, Eq)
data Dir = Dir Int Int deriving (Show, Eq)
data Sstate = Sstate {direction :: Dir, position :: Pos} deriving (Show, Eq)

-- A helper to simplify move
changeDir :: Mov -> Dir -> Dir
changeDir 'l' (Dir x y) = Dir (-y) x
changeDir 'r' (Dir x y) = Dir y (-x)
changeDir m (Dir x y) = Dir y x

move :: Mov -> Sstate -> Sstate
move 'f' oldState@(Sstate (Dir dx dy) (Pos px py))  = oldState { position = Pos (dx + px) (dy + py) }
move  m  oldState@(Sstate dir _) = oldState { direction = changeDir m dir }

end :: [Mov] -> Sstate -> Sstate
end ms initialState = foldl' (flip move) initialState ms

route :: [Mov] -> Sstate -> [Pos]
route ms initialState = map position $ scanl' (flip move) initialState ms

Then your examples become:

λ: let x = Sstate {direction = Dir 0 1, position = Pos 0 2}

λ: move 'f' x
Sstate {direction = Dir 0 1, position = Pos 0 3}

λ: end "ff" x
Sstate {direction = Dir 0 1, position = Pos 0 4}

λ: route "ff" x
[Pos 0 2,Pos 0 3,Pos 0 4]

I'll leave it as an exercise (or to someone more knowledgeable and less lazy than me) to adapt

move :: Mov -> Sstate -> Sstate
end :: [Mov] -> Sstate -> Sstate
route :: [Mov] -> Sstate -> [Pos]

to

move :: Mov -> State Sstate ()
-- or possibly move :: Mov -> State Sstate Sstate ?
-- Like I said, more knowledgeable than me
end :: [Mov] -> State Sstate Sstate
route :: [Mov] -> State Sstate [Pos]
like image 100
chepner Avatar answered Oct 03 '22 15:10

chepner


Here is some simple 'starter' code extending your module with some reformulations in terms of state. You will need to look at a tutorial like the LYAH chapter while fiddling with them, I'd think. I omit the signatures, which become increasingly complicated, but querying the types in ghci will be instructive. You need to add

import Control.Monad.State
import Control.Monad.Writer -- for the position-remembering example

Then the following should all work using your definition of move

step = do                        -- step the state once with your `move`,
 sstate <- get                   -- whatever the state is
 put (move sstate)
-- this little program could also be written: `modify move` which shows the
-- link between what you wrote and State a little more clearly

steps = do                       -- repeatedly apply `step` to the state
  Sstate _ _ ls <- get           -- til there are no moves, then stop
  if null ls 
  then return ()       -- could be: unless (null ls) $ do step ; steps
  else step >> steps

stepsIO = do                     -- do all steps as with `steps`, but print
  sstate@(Sstate a b ls) <- get  -- the current state at each state update
  liftIO $ print sstate
  if null ls then liftIO (putStrLn "Done!")
             else step >> stepsIO

stepsPrintPosition = do           -- do all steps as with `steps`, printing 
  Sstate _ b ls <- get            -- only position at each state update
  liftIO $ do putStr "current position: " 
              print b
  if null ls then liftIO (putStrLn "Done!")
             else do step 
                     stepsPrintPosition  

stepsAccumulatePositions = do    -- move through all states as with `steps`
  sstate@(Sstate a b ls) <- get  -- but use `tell` to keep adding the current
  tell [b]                       -- position to the underlying list 
  if null ls then return ()      -- of positions 
             else step >> stepsAccumulatePositions

example = Sstate (Dir 0 1) (Pos 0 2) "ffff"

To use things like step, steps, stepsIO etc, we apply runState; this gives us a function from a state to a new state

runStateT :: StateT s m a -> s -> m (a, s)

This of course is just the accessor for the newtype definition

newtype StateT s m a  = StateT {runStateT :: s -> m (a, s)}

The wrapping permits us to write fancy s -> m (a, s) things, using simpler s -> m (a, s) bits, but under the newtype hood, its always just a function s -> m (a, s) we are writing in the do notation.

Of course, once we unwrap with runStateT and have our function s -> m (a, s), we need to supply it with an initial state. It's easiest to see how this works by testing in ghci

>>> example
Sstate (Dir 0 1) (Pos 0 2) "ffff"

>>> runStateT step example            -- we step the state once with move
((),Sstate (Dir 0 1) (Pos 0 3) "fff")

>>> runStateT steps example           -- we keep stepping till there are no moves
((),Sstate (Dir 0 1) (Pos 0 6) "")

>>> runStateT stepsIO example         -- we print state at each state update
Sstate (Dir 0 1) (Pos 0 2) "ffff"
Sstate (Dir 0 1) (Pos 0 3) "fff"
Sstate (Dir 0 1) (Pos 0 4) "ff"
Sstate (Dir 0 1) (Pos 0 5) "f"
Sstate (Dir 0 1) (Pos 0 6) ""
Done!
((),Sstate (Dir 0 1) (Pos 0 6) "")

>>> runStateT stepsPrintPosition  example   -- print position only at state updates
current position: Pos 0 2
current position: Pos 0 3
current position: Pos 0 4
current position: Pos 0 5
current position: Pos 0 6
Done!
((),Sstate (Dir 0 1) (Pos 0 6) "") 


-- the WriterT examples accumulate a 'monoid' of things you keep
-- adding to with `tell xyz` Here we accumulate a [Position]
-- execXYZ and evalXYZ, where they exist, return less information than runXYZ

>>>  runWriterT $ runStateT stepsAccumulatePositions   example
(((),Sstate (Dir 0 1) (Pos 0 6) ""),[Pos 0 2,Pos 0 3,Pos 0 4,Pos 0 5,Pos 0 6])

>>>  execWriterT $ evalStateT stepsAccumulatePositions   example
[Pos 0 2,Pos 0 3,Pos 0 4,Pos 0 5,Pos 0 6]

In the code above I am using the mtl typeclasses and then using runStateT and runWriterT to "interpret" or specialize the class involving signatures. These are pertain to concrete types StateT and WriterT defined in Control.Monad.Trans.{State/Writer} One could omit the classes, and just write directly with those concrete types, importing those modules. The only difference would be that you need to do lift $ tell [b] in the one case where I combine two effects, state and writing or whatever you want to call it.

There is plenty to be said about the analysis of state you are working with but it will emerge how you might rework it, if you think the above through.

like image 38
Michael Avatar answered Oct 03 '22 15:10

Michael