Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell - is state monad a sign of imperative thinking?

I am writing a simple game - Tetris. For the first time in my life I'm using functional programming for that goal, as a language I chose Haskell. However, I'm tainted with OOP and imperative thinking and scared of unconsciously applying this mindset to my Haskell program.

Somewhere in my game, I need to have information about elapsed time (Timer) and pressed/down keys (Keyboard). The approach used in SDL lessons translated to Haskell looks like that:

Main.hs

data AppData = AppData {
    fps :: Timer 
    --some other fields    
}

getFPS :: MonadState AppData m => m Timer
getFPS = liftM fps get

putFPS :: MonadState AppData m => Timer -> m ()
putFPS t = modify $ \s -> s { fps = t }

modifyFPSM :: MonadState AppData m => (Timer -> m Timer) -> m ()
modifyFPSM act = getFPS >>= act >>= putFPS

Timer.hs

data Timer = Timer { 
    startTicks :: Word32,
    pausedTicks :: Word32,
    paused :: Bool,
    started :: Bool
}

start :: Timer -> IO Timer
start timer = SdlTime.getTicks >>= \ticks -> return $ timer { startTicks=ticks, started=True,paused=False }

isStarted :: Timer -> Bool
isStarted Timer { started=s } = s

And then used like that: modifyFPSM $ liftIO . start. That makes Timer somewhat pure (it is not explicitly a monad, and its functions return IO only because it's required to measure time). However, that litters the code outside Timer module with getters and setters.

My approach used in Keyboard.hs is:

data KeyboardState = KeyboardState {
    keysDown :: Set SDLKey, -- keys currently down
    keysPressed :: Set SDLKey -- keys pressed since last reset 
};

reset :: MonadState KeyboardState m => m ()
reset = get >>= \ks -> put ks{keysPressed = Data.Set.empty} 

keyPressed :: MonadState KeyboardState m => SDLKey -> m ()
keyPressed key = do
     ks <- get 
     let newKeysPressed = Data.Set.insert key $ keysPressed ks
     let newKeysDown = Data.Set.insert key $ keysDown ks
     put ks{keysPressed = newKeysPressed, keysDown = newKeysDown}

keyReleased :: MonadState KeyboardState m => SDLKey -> m ()
keyReleased key = do
     ks <- get 
     let newKeysDown = Data.Set.delete key $ keysDown ks
     put ks{keysDown = newKeysDown}

That makes the module self-contained, but I'm afraid that this is my way to express object from OOP in Haskell and ruins the whole point of FP. So my question is:

What is the proper way to do that? Or what are the other possibilities to approach such situation? And if you notice any other flaws (be it design or style problems) feel free to point that out.

like image 981
PL_kolek Avatar asked Dec 27 '13 15:12

PL_kolek


People also ask

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 is a Haskell 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.

Does Haskell have state?

The Haskell type State describes functions that consume a state and produce both a result and an updated state, which are given back in a tuple. Here, s is the type of the state, and a the type of the produced result.

What is the reader Monad?

The Reader monad (also called the Environment monad). Represents a computation, which can read values from a shared environment, pass values from function to function, and execute sub-computations in a modified environment. Using Reader monad for such computations is often clearer and easier than using the State monad.


1 Answers

Most programs have some notion of state. So you don't have to worry every time you use the State monad in some way shape or form. It is still purely function since you're essentially writing

Arg1 -> Arg2 -> State -> (State, Result)

But instead of writing your combinators of the state monad, instead consider writing them as simple pure functions and then using modify to inject them into the state monad.

reset :: KeyBoard -> KeyBoard
keyPressed :: Key -> KeyBoard -> KeyBoard
...

And then when you actually want state, these are easily used

 do
   nextKey <- liftIO $ magic
   modify $ keyPressed nextKey

And if you want to use them in pure functions, you're no longer dragging the whole state monad with them, making it a bit simpler to build up combinators.

TLDR: A little state is not bad, and can even make the code easier to understand, but dragging it into every part of your code is bad.

like image 185
Daniel Gratzer Avatar answered Nov 15 '22 08:11

Daniel Gratzer