Performing side effects during computation only once in Haskell

Here's a small library to cache values using an IORef. The function cache below transforms an IO a into another IO a which will only perform the side effects once, and return the cached value the next times it is executed.

import Data.IORef

cache :: IO a -> IO (IO a)
cache action = do
   v <- newIORef Nothing
   return $ do
      x <- readIORef v
      case x of
         Nothing -> do 
            res <- action
            writeIORef v $ Just res
            return res
         Just res -> return res

This is your function:

f :: Char -> IO Char
f x = print x >> return x

Here's a small example showing how to use cache.

main :: IO ()
main = do
   acts <- mapM (cache . f) "abc"
   -- here we have acts :: [IO Char]
   putStrLn "1st: "
   head acts               -- prints 'a'
   putStrLn "2nd: " 
   head acts               -- prints nothing
   putStrLn "3rd: "
   head acts               -- prints nothing
   return ()

Only the first call prints 'a' on screen.


Slightly unorthodox solution:

 λ> 
 λ> import System.IO.Unsafe
 λ> 
 λ> let { cache2 :: IO a -> IO a ; cache2 act = return (unsafePerformIO act) }
 λ> 
 λ> :t cache2
cache2 :: IO a -> IO a
 λ> 
 λ> let f x = print x >> return x
 λ> let a = map f "abc"
 λ>
 λ> :t a
a :: [IO Char]
 λ> 
 λ> 
 λ> a2 = map cache2 a
 λ> 
 λ> :t a2
a2 :: [IO Char]
 λ> 

Testing:

 λ> 
 λ> head a2
'a'
'a'
 λ> 
 λ> head a2
'a'
 λ> 
 λ> head a2
'a'
 λ> 
 λ> 
 λ> a2s = sequence a2
 λ> 
 λ> :t a2s
a2s :: IO [Char]
 λ> 
 λ> a2s
"a'b'
b'c'
c"
 λ> 
 λ> a2s
"abc"
 λ> 

Explanation: It is occasionally claimed that function unsafePerformIO :: IO a -> a offers a way to extract a value from the IO monad. The (sometimes omitted) catch is: how does the Haskell runtime system preserve referential transparency then ? well, simply by running the underlying computation just once, saving the result and discarding possible side effects of running the computation several times.

Edit:

As mentioned by Jon Purdy in the comments, in a general multithreaded context function unsafeInterleaveIO :: IO a -> IO a has to be preferred to unsafePerformIO. See the documentation for details.

Tags:

Haskell