猜我的号码,单子头疼
Guess My Number, a monadic headache
为了测试我在 Haskell 中的技能,我决定实现您在 Land of Lisp / Realm of 中找到的第一个游戏球拍。 "Guess My Number" 游戏。该游戏依赖于 运行 的可变状态,因为它必须不断更新程序的上限和下限以了解用户正在考虑的值。
它有点像这样:
> (guess)
50
> (smaller)
25
> (bigger)
37
现在,这种事情(据我所知)在 Haskell 中并不完全可能,从 REPL 调用一些函数来修改全局可变状态,然后立即打印结果,因为它违反了不变性原则。因此,所有交互都必须存在于 IO
and/or 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)
我现在需要做的就是想办法
- 打印初始猜测
- 收到命令要smaller/bigger号
- 相应地修改状态
- 递归调用函数,以便它再次猜测
如何以优雅的方式结合 IO
和 State
来实现这一点?
注意:我知道这可能根本不需要使用状态就可以实现;但我想让它保持原样
这是一个使用 StateT
转换器的解决方案。要点:
- 它使用
getLine
而不是使用 REPL 读取用户输入。
- 它读起来很像一个命令式程序,只是你必须在任何 IO 操作中添加
liftIO
。
- 您 运行 带有
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)
在此示例中,您根本不必使用 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。
您可以使用 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
你问的有点可能...
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
类型既有趣又有用。
为了测试我在 Haskell 中的技能,我决定实现您在 Land of Lisp / Realm of 中找到的第一个游戏球拍。 "Guess My Number" 游戏。该游戏依赖于 运行 的可变状态,因为它必须不断更新程序的上限和下限以了解用户正在考虑的值。
它有点像这样:
> (guess)
50
> (smaller)
25
> (bigger)
37
现在,这种事情(据我所知)在 Haskell 中并不完全可能,从 REPL 调用一些函数来修改全局可变状态,然后立即打印结果,因为它违反了不变性原则。因此,所有交互都必须存在于 IO
and/or 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)
我现在需要做的就是想办法
- 打印初始猜测
- 收到命令要smaller/bigger号
- 相应地修改状态
- 递归调用函数,以便它再次猜测
如何以优雅的方式结合 IO
和 State
来实现这一点?
注意:我知道这可能根本不需要使用状态就可以实现;但我想让它保持原样
这是一个使用 StateT
转换器的解决方案。要点:
- 它使用
getLine
而不是使用 REPL 读取用户输入。 - 它读起来很像一个命令式程序,只是你必须在任何 IO 操作中添加
liftIO
。 - 您 运行 带有
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)
在此示例中,您根本不必使用 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。
您可以使用 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
你问的有点可能...
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
类型既有趣又有用。