在 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
,但你不能使用它。有时显式递归是最好的,这似乎是其中一种情况。
我是 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
,但你不能使用它。有时显式递归是最好的,这似乎是其中一种情况。