猜猜我的号码,一个单一的头痛

时间:2015-01-27 19:21:04

标签: haskell state-monad io-monad

为了测试我在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)

我现在需要做的就是设计一种方法

  • 打印初始猜测
  • 接收需要更小/更大号码的命令
  • 相应地修改状态
  • 递归调用函数,以便再次猜测

如何以优雅的方式结合IOState来实现这一目标?

注意:我意识到这可能是在没有使用状态的情况下实现的;但我想让它忠实于原来的

4 个答案:

答案 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

您使用liftIOIO操作提升到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变换器的解决方案。值得注意的一点:

  1. 它使用getLine而不是使用REPL读取用户输入。
  2. 它非常像命令式程序,除非您必须将liftIO添加到任何IO操作中。
  3. 您使用runStateT运行循环,同时提供初始状态。
  4. 该计划:

    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)