Home > Enterprise >  Self-Mutating `IO a` in Haskell
Self-Mutating `IO a` in Haskell

Time:02-04

I would like to use IO Int to represent a stream of integers by hiding an IORef in its definition:

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

However, if I call ioTest n, rather than seeing an increasing list of numbers printed to the screen, I see only the starting number, n, repeating indefinitely.

While I could refactor this code so that incrementing and reading the value of ioIntTest i0 are done separately, I would like to know if/why the following is impossible:

Can I make an IO Int such that each time it is used in (>>=) (either explicitly or implicitly in do notation) the returned Int mutates?

While such an IO Int is perhaps not referentially transparent, I thought that was the point of wrapping computations in the IO monad.


Such a refactoring could be:

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

In other words, is there any way to replace the line ioIntMutate is in the third-to-last line with an IO Int?

CodePudding user response:

You can use IO (IO Int) for that. Like this:

ioIntTest :: Int -> IO (IO Int)
ioIntTest n = do
    ref <- newIORef n
    pure $ do
        i <- readIORef ref
        writeIORef ref (i 1)
        pure i

ioTest :: Int -> IO ()
ioTest n = do
    intStream <- ioIntTest n
    intStreamToPrint intStream
    where
        intStreamToPrint is = do
            threadDelay tickrate
            c <- is
            putStrLn (show c)
            intStreamToPrint is

Note that the only difference between my ioTest and your ioTest is this line:

let intStream = ioIntTest n -- yours
intStream <- ioIntTest n    -- mine

And, by the way, this solution is not so contrived. I have used a trick like this before to hide internal implementation details of an async RPC channel; or for another example on Hackage, check out once. You don't need to know whether that's implemented with IORefs or some other trick, and the author can switch tricks as they find better ones.

As a stylistic note, I'd write ioTest a little differently. One of these two:

ioTest :: Int -> IO ()
ioTest n = do
    intStream <- ioIntTest n
    forever (intStream >>= print >> threadDelay tickrate)

    -- OR

    forever $ do
        intStream >>= print
        threadDelay tickrate
  • Related