在 Haskell 中折叠用户输入

Folding Over User Input in Haskell

我是 Haskell 的新手,并且一直在编写一个简单的刽子手游戏来适应这门语言。我将游戏状态存储在名为 GameState 的数据类型中。

data GameState = GameState
  { word :: String,
    blanks :: String,
    wrongGuesses :: [Char]
  } deriving Show

我还有一个函数可以根据输入的字符更新状态,另一个函数可以检查游戏是否完成。

updateState :: GameState -> Char -> GameState
updateState state c
  | (c `elem` (word state)) = state { blanks = newBlanks }
  | otherwise = state { wrongGuesses = c:(wrongGuesses state)}
  where newBlanks = foldr replaceAt (blanks state) $ elemIndices c (word state)
        replaceAt i = (take i) <> (const [c]) <> (drop (i + 1))

isPlaying :: GameState -> Bool
isPlaying state = ('_' `elem` (blanks state)) && (length (wrongGuesses state) < 5)

给定一个角色列表,我可以做一个折叠来模拟给定输入的游戏。

runGame :: GameState -> [Char] -> [GameState]
runGame initialState chars = takeWhile isPlaying $ scanl updateState initialState chars

我的问题是如何调整此代码以适应实际用户输入。我试过使用序列,但程序无休止地获取用户输入。

main = runGame (newState "secret") <$> (sequence $ repeat getChar)

有没有什么方法可以像 runGame 一样折叠 getChar 并在每次用户输入后打印游戏状态?我想避免像 runGame 那样的显式递归,如果可能的话,我想坚持使用标准库函数。

第一:为什么你的程序不行?问题是,当您执行 repeat getChar 时,会无限地重复 getChar 。然后你使用 sequence;由于 sequence 的实现方式,它需要在返回之前遍历 整个 列表。但是那个列表是无限的!结果:无限循环。

现在,让我们尝试解决这个问题。在这种情况下,我做的第一件事通常是忘记 sequence 之类的函数,而只是将其写为递归函数:

main = runGameIO (newState "secret")
  where
    runGameIO :: GameState -> IO ()
    runGameIO curState = do
        input <- getChar
        let newState = updateState curState input
        if isPlaying newState        -- if still playing
            then runGameIO newState  -- then repeat with new state
            else return ()           -- else finish

现在,有什么办法可以简化runGameIO吗?我现在什么都看不到了。当然,我们现在可以看到 sequence 不会起作用:sequence 要求给它的每个项目都是独立的,即每个 monadic 动作不依赖于先前的 monadic 动作。 (这就是为什么你也可以用 Applicative 约束而不是 Monad 约束来实现 sequence 的原因。)但是这个问题都是 about 给出的响应以前的状态!很明显 sequence 行不通。

接下来,让我们对 do-notation 进行脱糖,看看我们是否能发现任何东西:

runGameIO :: GameState -> Io ()
runGameIO curState =
    getChar >>= \input ->
    let newState = updateState curState input
    in if isPlaying newState then runGameIO newState else return ()

嗯……还是没什么,真的。但这使您可以在此处使用 fmap 变得更加明显:

runGameIO :: GameState -> Io ()
runGameIO curState =
    fmap (updateState curState) getChar >>= \newState ->
    if isPlaying newState then runGameIO newState else return ()

在这一点上,通常有一些巧妙的技巧可以用来摆脱显式递归。但我看不到这里有什么可做的。如果你不限制自己使用标准库,我会说使用 monad-loops 包中的 iterateUntilM,但你不能使用它。有时显式递归是最好的,这似乎是其中一种情况。