I'm trying to understand how IORefs
are really used, and I'm having trouble following the sample code I found on https://www.seas.upenn.edu/~cis194/spring15/lectures/12-unsafe.html
newCounter :: IO (IO Int)
newCounter = do
r <- newIORef 0
return $ do
v <- readIORef r
writeIORef r (v + 1)
return v
printCounts :: IO ()
printCounts = do
c <- newCounter
print =<< c
print =<< c
print =<< c
When printCounts
executes "c <- newCounter
", why doesn't c
get the result of doing the work in the newCounter
"return $ do
" block, which seems like it should get assigned to the constant "IO 0
" the first time it is called and then never change? Instead, c
seems to get assigned the function defined in that "return $ do
" block, which is then executed anew every time printCounts
gets to another "print =<< c
." It seems that the answer somehow lies in newCounter
having the double nested "IO (IO Int)
" type, but I can't follow why that makes c
a function to be re-executed when called instead of a constant evaluated just once.
You can think of
IO
as a type of programs.newCounter :: IO (IO Int)
is a program that outputs a program. More precisely,newCounter
allocates a new counter, and returns a program that, when run, increments the counter and returns its old value.newCounter
doesn't execute the program it returns. It would if you wrote instead:You can also use equational reasoning to unfold
printCounts
into a sequence of primitives. All versions ofprintCounts
below are equivalent programs:In the final version, you can see that
printCounts
quite literally allocates a counter and increments it three times, printing each intermediate value.One key step is the let-substitution one, where the counter program gets duplicated, which is why it gets to run three times.
let x = p; ...
is different fromx <- p; ...
, which runsp
, and bindsx
to the result rather than the programp
itself.