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
为什么满足要求?
- 无法作弊:秘密数字
secret被封装在playerA创建的闭包里,playerB只能通过runGuessFunc获取回应,完全看不到secret的值。 - 严格限制调用次数:每次调用
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
相关产品推荐
相关产品推荐

