哈斯克尔 - 状态monad是势在思考的标志吗?

时间:2013-12-27 15:35:15

标签: oop haskell game-engine monads state-monad

我正在写一个简单的游戏 - 俄罗斯方块。在我生命中我第一次使用函数式编程来实现这个目标,作为我选择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的整个点。所以我的问题是:

这样做的正确方法是什么?或者接近这种情况的其他可能性是什么?如果您发现任何其他缺陷(无论是设计还是样式问题),请随时指出。

2 个答案:

答案 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非常善于抽象,并且允许您在游戏中以更高的级别表达您想要的概念。您可能希望研究“功能反应式编程”