PL_*_*lek 5 oop monads haskell state-monad game-engine
我正在写一个简单的游戏 - 俄罗斯方块.在我生命中我第一次使用函数式编程来实现这个目标,作为我选择Haskell的语言.然而,我被OOP和命令式思维所污染,害怕无意识地将这种心态运用到我的Haskell程序中.
在我游戏的某个地方,我需要有关于经过时间(计时器)和按下/向下键(键盘)的信息.转换为Haskell的SDL课程中使用的方法如下所示:
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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
然后就这样使用:modifyFPSM $ liftIO . start
.这使得Timer有点纯粹(它不是显式的monad,它的函数只返回IO,因为它需要测量时间).但是,那些带有getter和setter的Timer模块外部的代码.
我在Keyboard.hs中使用的方法是:
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}
Run Code Online (Sandbox Code Playgroud)
这使得模块自成一体,但我担心这是我在Haskell中从OOP表达对象的方式,并且破坏了FP的整个点.所以我的问题是:
这样做的正确方法是什么?或者接近这种情况的其他可能性是什么?如果您发现任何其他缺陷(无论是设计还是样式问题),请随时指出.
大多数程序都有一些国家概念.因此,每次State
以某种形式或形式使用monad时,您都不必担心.它仍然是纯粹的功能,因为你基本上是在写作
Arg1 -> Arg2 -> State -> (State, Result)
Run Code Online (Sandbox Code Playgroud)
但是不是编写状态monad的组合器,而是考虑将它们编写为简单的纯函数,然后使用modify
它们将它们注入状态monad.
reset :: KeyBoard -> KeyBoard
keyPressed :: Key -> KeyBoard -> KeyBoard
...
Run Code Online (Sandbox Code Playgroud)
然后当你真正想要状态时,这些很容易使用
do
nextKey <- liftIO $ magic
modify $ keyPressed nextKey
Run Code Online (Sandbox Code Playgroud)
如果你想在纯函数中使用它们,你不再用它们拖动整个状态monad,使得构建组合器更简单一些.
TLDR:一个小状态并不坏,甚至可以使代码更容易理解,但将其拖入代码的每个部分都是不好的.