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.
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With