5

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.

daniel gratzer
  • 52,833
  • 11
  • 94
  • 134
PL_kolek
  • 325
  • 3
  • 9
  • Some thoughts unrelated to your question: 1. Don't assume "monad" is an antonym of "pure". All (correct) monadic code should be pure, in fact. `IO` explains how to construct an impure program, but the `IO` values themselves are pure. 2. In most timers, it makes no sense to say that it's paused if it hasn't been started. In order to eliminate this as a possible state the value could be in, you could replace those two fields with a single field containing something like `data TimerStatus = Stopped | Running | Paused`. – Carl Dec 27 '13 at 16:21
  • 1
    Functional Reactive Programming is the most functional approach for what you're doing. If you want to learn to think in new ways, go to Haskell and do FRP. Use Applicative syntax wherever you can instead of monadic syntax. Use higher order functions and pointfree style whenever it's not convoluted. – not my job Dec 27 '13 at 22:34
  • I considered FRP, but chose not to dive that deep in FP in my first project. Understanding Haskell enough to write a game was much, Haskell + FRP would be too much for start. – PL_kolek Dec 28 '13 at 11:41

2 Answers2

7

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.

daniel gratzer
  • 52,833
  • 11
  • 94
  • 134
2

Contrary to popular belief, the Haskell philosophy is not about eliminating state, but about making state explicit, encapsulating it and controlling it. Feel free to use the state monad to your heart's content, if it makes your code clearer.

Haskell is very good at abstraction, and will allow you to express the concepts you want in your game at a higher level that what you have here. You would probably like to look into "Functional Reactive Programming"

Tom Ellis
  • 9,224
  • 1
  • 29
  • 54
  • Then how is it different from, for example, Java? Encapsulation is a part of OOP and the point of it is to control the state. When I made that Keyboard module, it felt too much like regular Java object with setters and state encapsulated in State monad. – PL_kolek Dec 27 '13 at 18:49
  • 1
    Good question. OOP originated as another approach with this aim. In my opinion the Haskell philosophy is more satisfying because it makes state explicit. I edited my answer to mention the explicitness of state. You probably also would like to look into FRP. – Tom Ellis Dec 27 '13 at 19:35