为了测试我在Haskell的技能,我决定实现你在 Lisp的土地 / 领域的中找到的第一款游戏。 "猜猜我的号码" 游戏。游戏依赖于可变状态来运行,因为它经常需要更新程序的上限和下限以回归用户所想的值。
它有点像这样:
> (guess)
50
> (smaller)
25
> (bigger)
37
现在,这种事情(据我所知)在Haskell中完全不可能,从REPL调用一些修改全局可变状态的函数,然后立即打印结果,因为它违反了不变性原则。因此,所有互动都必须位于IO
和/或State
monad内。那就是我被困的地方。
我似乎无法将IO
monad和State
monad组合在一起,所以我可以获得输入,打印结果和修改状态同样的功能。
这是我到目前为止所得到的:
type Bound = (Int, Int) -- left is the lower bound, right is the upper
initial :: Bound
initial = (1, 100)
guess :: Bound -> Int
guess (l, u) = (l + u) `div` 2
smaller :: State Bound ()
smaller = do
bd@(l, _) <- get
let newUpper = max l $ pred $ guess bd
put $ (l, newUpper)
bigger :: State Bound ()
bigger = do
bd@(_, u) <- get
let newLower = min u $ succ $ guess bd
put $ (newLower, u)
我现在需要做的就是设计一种方法
如何以优雅的方式结合IO
和State
来实现这一目标?
注意:我意识到这可能是在没有使用状态的情况下实现的;但我想让它忠实于原来的
答案 0 :(得分:12)
您可以使用monad变换器组合不同的monad - 在本例中为StateT
。您可以通过更改类型签名来使用现有代码以使用StateT
:
bigger, smaller :: Monad m => StateT Bound m ()
然后你可以编写一个函数来给出一个状态参数来运行游戏:
game :: StateT Bound IO ()
game = do
s <- get
liftIO $ print (guess s)
verdict <- (liftIO getLine)
case verdict of
"smaller" -> smaller >> game
"bigger" -> bigger >> game
"ok" -> return ()
_ -> (liftIO $ putStrLn $ "Unknown verdict " ++ verdict) >> game
您使用liftIO
将IO
操作提升到StateT Bound IO
monad,允许您提示输入并阅读下一行。
最后,您可以使用runStateT
:
runStateT game initial
答案 1 :(得分:7)
你问的是有可能......
import Data.IORef
makeGame :: IO (IO (), IO (), IO ())
makeGame = do
bound <- newIORef (1, 100)
let guess = do
(min, max) <- readIORef bound
print $ (min + max) `div` 2
smaller = do
(min, max) <- readIORef bound
let mid = (min + max) `div` 2
writeIORef bound (min, mid)
guess
bigger = do
(min, max) <- readIORef bound
let mid = (min + max) `div` 2
writeIORef bound (mid, max)
guess
return (guess, smaller, bigger)
没关系代码中有多少冗余,这只是概念的快速证明。这是一个示例会话:
$ ghci guess.hs
GHCi, version 7.9.20141202: http://www.haskell.org/ghc/ :? for help
[1 of 1] Compiling Main ( guess.hs, interpreted )
Ok, modules loaded: Main.
*Main> (guess, smaller, bigger) <- makeGame
*Main> guess
50
*Main> smaller
25
*Main> bigger
37
*Main>
嵌套IO
类型非常有趣且有用。
答案 2 :(得分:2)
在此示例中,您根本不必使用State monad。以下是将状态作为参数传递的示例:
loop :: Bound -> IO ()
loop bd@(l,u) = do
putStr "> "
line <- getLine
case line of
"(guess)" -> print (guess bd) >> loop bd
"(smaller)" -> do
let newUpper = max l $ dec $ guess bd
print $ guess (l, newUpper)
loop (l, newUpper)
"(bigger)" -> do
let newLower = min u $ inc $ guess bd
print $ guess (newLower, u)
loop (newLower, u)
"" -> return ()
_ -> putStrLn "Can't parse input" >> loop bd
main :: IO ()
main = loop initial
否则,您正在寻找的概念是 monad transformers 。例如,使用StateT:
smaller :: StateT Bound IO ()
smaller = do
bd@(l, _) <- get
let newUpper = max l $ dec $ guess bd
put $ (l, newUpper)
bigger :: StateT Bound IO ()
bigger = do
bd@(_, u) <- get
let newLower = min u $ inc $ guess bd
put $ (newLower, u)
guessM :: StateT Bound IO ()
guessM = get >>= lift . print . guess
loop :: StateT Bound IO ()
loop = do
lift $ putStr "> "
line <- lift getLine
case line of
"(guess)" -> guessM >> loop
"(smaller)" -> do
smaller
guessM
loop
"(bigger)" -> do
bigger
guessM
loop
"" -> return ()
_ -> lift (putStrLn "Can't parse input") >> loop
main :: IO ()
main = evalStateT loop initial
有关monad变换器主题的教程,请参阅此chapter of Real World Haskell。
答案 3 :(得分:1)
这是使用StateT
变换器的解决方案。值得注意的一点:
getLine
而不是使用REPL读取用户输入。liftIO
添加到任何IO操作中。runStateT
运行循环,同时提供初始状态。该计划:
import Control.Monad.State
loop :: StateT (Int,Int) IO ()
loop = do
(lo,hi) <- get
let g = div (lo+hi) 2
liftIO $ putStrLn $ "I guess " ++ show g
ans <- liftIO getLine
case ans of
"lower" -> do put (lo,g); loop
"higher" -> do put (g,hi); loop
"exact" -> return ()
_ -> do liftIO $ putStrLn "huh?"; loop
main = runStateT loop (0,50)