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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 15:30:22