【问题标题】:Playing aroud with Haskell State玩弄 Haskell State
【发布时间】:2013-11-13 23:44:23
【问题描述】:

所以我正在尝试使用 State 来实现 Haskell 游戏,作为游戏的一部分,我想实现一些方法来保存当前玩家的姓名并在调用时检索它。我有辅助函数 popStack 和 pushStack 分别将值弹出和推送到堆栈中。

当前代码:

 import Control.Monad.State

 data Gamestate = Gamestate {
     gamestack :: [String],
     gamememory :: String
 }

 type NewGameState = State GameState

 popStack :: NewGameState String
 popStack = state $ \st -> case gamestack st of
     [] -> (0.0,st)
     x:xs -> (x,st { gamestack = xs })

 pushStack :: String -> NewGameState ()
 push d = modify  $ \st -> st { gamestack = d : gamestack st }

我为 saveName 和 getName 提供了以下代码。

saveName :: NewGameState ()
saveName = do
        memory <-head   
        pushStack $ x

getName :: NewGameState ()
getName = do
        memory <- head gamestack
        popStack $ memory

上面的代码 sn-ps 返回类型错误。我不太了解状态单子。那么如何使用 saveName 将游戏栈顶部的当前玩家名称复制到游戏内存中,并在使用 getName 时将游戏内存推送到游戏栈顶部?

对不起,如果它有点混乱。我是一名 ESL 演讲者。提前致谢。

【问题讨论】:

  • 您应该包含您收到的错误消息,但是我注意到您在saveNamememory &lt;- head 中有一个明显的错误。你处于状态单子,而不是函数单子。
  • 为什么你认为你需要一个堆栈?键/值存储(可能是 Map)可能会更有用。堆栈允许您推送多个对象;如果你这样做,你将如何从堆栈中的其他东西下检索玩家的名字?你怎么知道哪些项目是哪些?如果你只把名字放在那里,没有别的,为什么还要麻烦堆栈呢?

标签: haskell functional-programming state


【解决方案1】:

我将通过向您展示做您想做的事情的惯用方式来回答您的问题。随着我的进展,我将指出我在您的代码中修复的内容。

第一个问题:Gamestate 的大小写不一致。大写在 Haskell 中很重要,所以我将所有内容重命名为 GameState

因此,在进行修复后,我做的第一件事就是为您的两个数据类型的字段定义镜头。这使得修改状态子集的有状态事情变得更加容易。当我了解其余功能的实现时,您会看到这一点:

import Control.Monad.State
import Control.Lens

data GameState = GameState
    { _gamestack  :: [String]
    , _gamememory ::  String
    }

gamestack :: Lens' GameState [String]
gamestack k (GameState s m) = fmap (\s' -> GameState s' m) (k s)

gamememory :: Lens' GameState String
gamememory k (GameState s m) = fmap (\m' -> GameState s m') (k m)

type NewGameState = State GameState

请注意,您不必像这样手动定义镜头。除了定义gamememorygamestack,您也可以这样做:

{-# LANGUAGE TemplateHaskell #-}  -- Note the extension

import Control.Lens

data GameState = GameState
    { _gamestack  :: [String]
    , _gamememory ::  String
    }

makeLenses ''GameState

无论您选择哪种方式,一旦我们有了这些镜头,我们就可以编写pushpop,这样他们就不会关心他们正在做什么状态,只要它是一个列表:

pop :: State [a] (Maybe a)
pop = do
    s <- get
    case s of
        []   -> return Nothing
        x:xs -> do
            put xs
            return (Just x)

push :: a -> State [a] ()
push d = modify (d:)

请注意,如果列表为空,我将 pop 更改为返回 Maybe。这比默认使用 0 或使用 head 更惯用 Haskell。

使用pushpop,在游戏内存和堆栈之间传输值变得非常容易:

saveName :: NewGameState ()
saveName = do
    memory <- use gamememory
    zoom gamestack (push memory)

getName :: NewGameState ()
getName = do
    m <- zoom gamestack pop
    case m of
        Nothing -> return ()
        Just x  -> gamememory .= x

请注意我如何使用 zoom 本地化 pushpop 以对 gamememorygamestack 字段进行操作。 zoom 将镜头带到一个子字段,然后运行有状态的操作,就好像整个状态就是那个子字段一样。这很酷,因为现在 pushpop 可重用性更高,我们不必在其中添加特定的状态数据类型选择。

这也使用了.=,它设置了一个给定的字段。基本上是一样的:

lens .= x = zoom lens (put x)

要了解有关镜头(.=)zoom 的更多信息,您可能需要阅读我写的this post

编辑:根据要求,这是无镜头版本:

import Control.Monad.State

data GameState = GameState
    { gamestack  :: [String]
    , gamememory ::  String
    }

type NewGameState = State GameState

saveName :: NewGameState ()
saveName = do
    GameState stack memory <- get
    put (GameState (memory:stack) memory)

getName :: NewGameState ()
getName = do
    GameState stack memory <- get
    case stack of
        []   -> put (GameState stack memory)
        x:xs -> put (GameState xs    x     )

【讨论】:

  • 我会犹豫向初学者展示镜头。我喜欢这个图书馆,但我们有漫画说它对初学者来说界面很差
  • @jozefg 通常我会同意,但zoom 是更新嵌套字段的唯一优雅方式。
  • 这太棒了!但是我只是一个初学者,想知道是否有一种更简单、不优雅的方式来编写 getName 函数而不使用镜头和缩放?
  • @PaulK 我在答案末尾添加了无镜头版本。
【解决方案2】:

如果某个东西在&lt;- 的右侧,那么它必须在那个 monad 中。所以你想要的就是这样的

saveName :: NewGameState ()
saveName = do
  memory <- fmap gamememory get
  pushStack memory

getName = popStack

对于saveName,我们fmapgamememory 覆盖当前状态并将结果存储在memory 中,而不是将其压入堆栈。如果你想花哨的话,我们实际上可以写成get &gt;&gt;= pushStack . gamememory

popStack 不接受任何参数,所以我不确定你想要什么。我最好的猜测是它应该只获取我们推送的姓氏,它只是调用popStack

【讨论】:

  • 澄清一下,getName 动作应该将游戏内存推到游戏堆栈的顶部。
【解决方案3】:

NewGameState 是一个糟糕的名字——它根本不是一个新的游戏状态,它是一个带有状态的单子。我只是叫它Game

pushStack vs push - 你给出了一个名为 pushStack 的签名,然后是一个名为 push 的函数。选择一个。

popStack 中有[] -&gt; (0.0, st) 让我们面对现实吧,0.0 不是字符串,那你为什么要返回它呢?您只是不知道弹出空堆栈时该怎么做吗?不如改用""

saveName 和 getName 好吧,你甚至还没有说你想要这些做什么。看来您接受了其他回答者的解释,因此,我们可以使用记录更新语法。

最后,这是一些至少可以编译的代码:

import Control.Monad.State

data GameState = GameState {
    gamestack :: [String],
    gamememory :: String
}

type Game = State GameState

popStack :: Game (Maybe String)
popStack = state $ \st -> case gamestack st of
    [] -> (Nothing,st)
    x:xs -> (Just x,st { gamestack = xs })

pushStack :: String -> Game ()
pushStack d = modify  $ \st -> st { gamestack = d : gamestack st }

saveName :: Game ()
saveName = do
    memory <- gamememory `fmap` get
    pushStack memory

getName :: Game ()
getName = do
    newMem <- popStack
    case newMem of
       Nothing -> return ()
       Just n  -> modify (\x -> x { gamememory = n } )

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-08-20
    • 2013-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多