我正在写一个简单的游戏 - 俄罗斯方块。在我生命中我第一次使用函数式编程来实现这个目标,作为我选择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
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
然后像那样使用:modifyFPSM $ liftIO . start
。这使得Timer有点纯粹(它不是显式的monad,它的函数只返回IO,因为它需要测量时间)。但是,这会使得定时器模块外部的代码充满了getter和setter。
我在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}
这使得模块自包含,但我担心这是我在Haskell中从OOP表达对象的方式并且破坏了FP的整个点。所以我的问题是:
这样做的正确方法是什么?或者接近这种情况的其他可能性是什么?如果您发现任何其他缺陷(无论是设计还是样式问题),请随时指出。
答案 0 :(得分:7)
大多数程序都有一些国家概念。因此,每次以某种形式或形式使用State
monad时,您都不必担心。它仍然是纯粹的功能,因为你基本上是在写
Arg1 -> Arg2 -> State -> (State, Result)
但是不要编写状态monad的组合器,而是考虑将它们编写为简单的纯函数,然后使用modify
将它们注入状态monad。
reset :: KeyBoard -> KeyBoard
keyPressed :: Key -> KeyBoard -> KeyBoard
...
然后当你真正想要状态时,这些很容易使用
do
nextKey <- liftIO $ magic
modify $ keyPressed nextKey
如果你想在纯函数中使用它们,你不再用它们拖动整个状态monad,使得构建组合器变得更简单。
TLDR:一个小状态也不错,甚至可以让代码更容易理解,但将其拖入代码的每个部分都是不好的。
答案 1 :(得分:2)
与流行的看法相反,Haskell哲学不是关于消除状态,而是关于使状态显式化,封装它并控制它。如果它使您的代码更清晰,请随意使用状态monad到您的内心。
Haskell非常善于抽象,并且允许您在游戏中以更高的级别表达您想要的概念。您可能希望研究“功能反应式编程”