Haskell中能否创建每次调用都会自变的IO Int?
用IO Int实现可复用的递增整数流
问题描述
我尝试通过隐藏IORef,用IO Int表示整数流,写了如下代码:
tickrate :: Int tickrate = 20000 ioIntTest :: Int -> IO Int ioIntTest i0 = do intRef <- newIORef i0 f intRef where f :: IORef Int -> IO Int f ref = do i <- readIORef ref modifyIORef ref (+1) return i ioTest :: Int -> IO () ioTest n = do let intStream = ioIntTest n intStreamToPrint intStream where intStreamToPrint is = do threadDelay tickrate c <- is putStrLn (show c) intStreamToPrint is
但调用ioTest n时,屏幕上持续输出初始值n,而非递增的数字。虽然可以通过重构代码将递增与读取操作分离(如下):
tickrate :: Int tickrate = 20000 ioIntMutate :: IORef Int -> IO Int ioIntMutate ref = do i <- readIORef ref modifyIORef ref (+1) return i ioTest :: Int -> IO () ioTest n = do intStream <- newIORef n intStreamToPrint intStream where intStreamToPrint is = do threadDelay tickrate c <- ioIntMutate is putStrLn (show c) intStreamToPrint is
但我想知道:能否创建一个IO Int,使其每次通过>>=(或do记法)使用时,返回的Int都会递增? 换句话说,能不能用这个IO Int替换上面重构代码中倒数第三行的ioIntMutate is?我原以为IO monad就是用来处理这类非引用透明的场景的。
问题原因分析
初始代码的问题在于:let intStream = ioIntTest n定义的intStream是一个IO Int动作,每次执行这个动作都会完整跑一遍ioIntTest的逻辑——也就是每次都会新建一个IORef、读取初始值n、加1后返回。每次执行都是独立的,没有共享状态,所以自然一直输出初始值。
解决方案:创建带共享状态的IO Int
可以实现一个函数,先创建一次IORef,然后返回一个捕获了该IORef的IO Int动作,这个动作每次执行都会复用同一个IORef,从而实现递增效果:
tickrate :: Int tickrate = 20000 mkIncrementingStream :: Int -> IO (IO Int) mkIncrementingStream i0 = do ref <- newIORef i0 -- 返回一个共享ref的IO动作 return $ do i <- readIORef ref modifyIORef' ref (+1) -- 使用modifyIORef'避免惰性求值问题 return i ioTest :: Int -> IO () ioTest n = do intStream <- mkIncrementingStream n -- 这里拿到的是带共享状态的IO Int intStreamToPrint intStream where intStreamToPrint is = do threadDelay tickrate c <- is -- 每次执行is都会操作同一个IORef putStrLn (show c) intStreamToPrint is
核心逻辑说明
mkIncrementingStream先执行一次IO动作创建IORef,然后返回一个闭包式的IO动作——这个动作捕获了创建好的IORef,每次执行时都会读取当前值、递增并返回。- 与初始代码的区别在于:IORef只创建一次,后续所有对
intStream的调用都复用这个IORef,状态得以保留,从而实现每次返回递增的整数。
这样,我们就得到了一个可以直接复用的IO Int,完全可以替换重构代码中ioIntMutate is的位置。
内容的提问来源于stack exchange,提问作者Anthony D'Arienzo
相关产品推荐
相关产品推荐

