【问题标题】:Haskell - is state monad a sign of imperative thinking?Haskell - 状态单子是命令式思维的标志吗?
【发布时间】:2013-12-27 15:35:15
【问题描述】:

我正在编写一个简单的游戏——俄罗斯方块。这是我有生以来第一次使用函数式编程来实现这个目标,我选择了 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

定时器.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 只是因为它需要测量时间)。但是,这会在 Timer 模块之外的代码中添加 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 的全部意义。所以我的问题是:

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

【问题讨论】:

  • 与您的问题无关的一些想法: 1. 不要假设“monad”是“pure”的反义词。事实上,所有(正确的)一元代码都应该是纯代码。 IO 解释了如何构造一个不纯的程序,但 IO 值本身是纯的。 2. 在大多数定时器中,如果它没有启动就说它暂停是没有意义的。为了消除此值可能处于的可能状态,您可以将这两个字段替换为包含 data TimerStatus = Stopped | Running | Paused 之类的单个字段。
  • 函数响应式编程是最实用的方法。如果你想学习用新的方式思考,去 Haskell 做 FRP。尽可能使用应用语法,而不是一元语法。只要不复杂,就使用高阶函数和无点样式。
  • 我考虑过 FRP,但在我的第一个项目中选择不深入研究 FP。对 Haskell 的理解足以编写一个游戏已经够多了,Haskell + FRP 对于开始来说就太难了。

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


【解决方案1】:

大多数程序都有一些状态概念。因此,每次以某种形式或形式使用 State monad 时,您不必担心。它仍然是纯粹的函数,因为你本质上是在写

Arg1 -> Arg2 -> State -> (State, Result)

但不要编写状态单子的组合器,而是考虑将它们编写为简单的纯函数,然后使用 modify 将它们注入到状态单子中。

reset :: KeyBoard -> KeyBoard
keyPressed :: Key -> KeyBoard -> KeyBoard
...

然后当你真正想要状态时,这些很容易使用

 do
   nextKey <- liftIO $ magic
   modify $ keyPressed nextKey

如果你想在纯函数中使用它们,你不再需要用它们来拖拽整个 state monad,这使得构建组合器变得更简单。

TLDR:一点点状态也不错,甚至可以让代码更容易理解,但是把它拖到代码的每一部分都是不好的。

【讨论】:

    【解决方案2】:

    与流行的看法相反,Haskell 哲学不是关于消除状态,而是关于使状态明确化、封装和控制它。如果你的代码更清晰,请随意使用 state monad。

    Haskell 非常擅长抽象,它可以让您在游戏中表达您想要的概念,比您现在拥有的更高层次。您可能想研究“函数式反应式编程”

    【讨论】:

    • 那么它与例如Java有什么不同呢?封装是 OOP 的一部分,其目的是控制状态。当我制作那个 Keyboard 模块时,感觉它太像带有 setter 和 state 封装在 State monad 中的常规 Java 对象了。
    • 好问题。 OOP 起源于另一种具有此目的的方法。在我看来,Haskell 哲学更令人满意,因为它使状态明确。我编辑了我的答案以提及状态的明确性。您可能还想研究 FRP。
    猜你喜欢
    • 2018-07-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-26
    • 1970-01-01
    • 2019-01-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多