You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Haskell纯函数式实现限次猜数字游戏:无需IORef可行吗?

纯函数式实现猜数字游戏(无IORef)

当然可以!我们完全能不用IORef这类可变引用,用纯函数式的思路实现这个猜数字游戏。核心就是把「剩余尝试次数」这个状态显式传递,而不是靠可变变量来偷偷修改——这也是Haskell纯函数式编程处理状态的经典方式~

核心思路

原来用IORef是靠可变变量维护剩余次数,纯函数式方案里,我们可以把剩余次数封装在一个不可变的函数实例中:每次调用检查函数后,返回一个携带更新后剩余次数的新函数,后续猜测必须使用这个新函数。这样既保证了playerB无法访问秘密数字,也严格限制了调用次数。

方案1:闭包传递状态

我们先定义一个GuessFunc类型,用来封装检查函数和剩余次数状态:

newtype GuessFunc = GuessFunc { runGuessFunc :: Int -> IO (Maybe Ordering, GuessFunc) }

然后实现playerA和playerB:

import System.Random (randomRIO)

lowest :: Int
lowest = 1
highest :: Int
highest = 100
maxTries :: Int
maxTries = 5

playerA :: IO GuessFunc
playerA = do
    secret <- randomRIO (lowest, highest)
    putStrLn $ "Secret number: " ++ show secret
    -- 递归构造GuessFunc,每次调用返回新的实例(剩余次数减一)
    let makeGuessFunc remaining = GuessFunc $ \guess ->
            if remaining == 0
                then return (Nothing, makeGuessFunc 0)  -- 次数用完后,所有调用都返回Nothing
                else do
                    let response = Just $ guess `compare` secret
                    return (response, makeGuessFunc (remaining - 1))
    return $ makeGuessFunc maxTries

playerB :: GuessFunc -> Int -> Int -> IO ()
playerB guessFunc low high = do
    let guess = (low + high) `div` 2
    -- 调用检查函数,获取回应和新的GuessFunc
    (response, newGuessFunc) <- runGuessFunc guessFunc guess
    putStrLn $ "Bounds: (" ++ show low ++ ", " ++ show high ++ "), Guess: " ++ show guess ++ ", Response: " ++ show response
    case response of
        Just GT -> playerB newGuessFunc low (guess - 1)
        Just LT -> playerB newGuessFunc (guess + 1) high
        Just EQ -> putStrLn "Player B wins!"
        Nothing -> putStrLn "Player B loses - out of tries!"

main :: IO ()
main = do
    gf <- playerA
    playerB gf lowest highest

为什么满足要求?

  1. 无法作弊:秘密数字secret被封装在playerA创建的闭包里,playerB只能通过runGuessFunc获取回应,完全看不到secret的值。
  2. 严格限制调用次数:每次调用runGuessFunc都会返回一个新的GuessFunc,其中剩余次数减一。当次数用完后,所有后续调用都会返回Nothing,playerB无法绕过这个限制——因为它只能使用每次返回的新函数进行下一次猜测。

方案2:用StateT简化状态管理

如果觉得手动传递GuessFunc有点繁琐,可以用StateT monad来封装剩余次数的状态转换,代码会更简洁:

import Control.Monad.State (StateT, runStateT, get, put, liftIO)
import System.Random (randomRIO)

lowest :: Int
lowest = 1
highest :: Int
highest = 100
maxTries :: Int
maxTries = 5

playerA :: IO (Int -> StateT Int IO (Maybe Ordering))
playerA = do
    secret <- randomRIO (lowest, highest)
    putStrLn $ "Secret number: " ++ show secret
    return $ \guess -> do
        remaining <- get  -- 获取当前剩余次数
        if remaining == 0
            then return Nothing
            else do
                put (remaining - 1)  -- 更新剩余次数
                return $ Just $ guess `compare` secret

playerB :: (Int -> StateT Int IO (Maybe Ordering)) -> Int -> Int -> StateT Int IO ()
playerB guessFunc low high = do
    let guess = (low + high) `div` 2
    response <- guessFunc guess
    liftIO $ putStrLn $ "Bounds: (" ++ show low ++ ", " ++ show high ++ "), Guess: " ++ show guess ++ ", Response: " ++ show response
    case response of
        Just GT -> playerB guessFunc low (guess - 1)
        Just LT -> playerB guessFunc (guess + 1) high
        Just EQ -> liftIO $ putStrLn "Player B wins!"
        Nothing -> liftIO $ putStrLn "Player B loses - out of tries!"

main :: IO ()
main = do
    gf <- playerA
    -- 初始状态是maxTries,运行playerB的逻辑
    runStateT (playerB gf lowest highest) maxTries
    return ()

这个方案里,StateT Int IO把剩余次数作为状态,get和put操作都是纯的状态转换,没有使用任何可变引用,同样满足你的所有要求。

总结

两种方案都是纯函数式的,核心都是用不可变的状态转换替代可变变量:

  • 方案1用闭包显式传递状态,更直观,不需要依赖额外的monad库。
  • 方案2用StateT封装状态管理,代码更简洁,适合复杂状态的场景。

内容的提问来源于stack exchange,提问作者Retired Writing Code for Fun

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:25:26