“Haskell is the world’s finest imperative programming language.”
– Simon Peyton-Jones 2001
What if you’re trying to create a global counter? If you need some global cache and just don’t want to plumb an IORef
through all of your code? Is there some dirty hack you can use to get a global IORef
?
I will show you that there is and it’s a very handy technique to have at your disposal when you need it. It was first published by Simon himself in 2001, so you shouldn’t feel too bad about using it! (note: you should feel a little bit bad…)
Jump right to the complete code if you just want to copy/paste.
Haskell is generally known for its dedication to purely functional programming, but sometimes it can be convenient and maybe even necessary to cheat the system.
“Many imperative algorithms require global variables.”
– John Hughes 2004
Let’s look at the simplest case of wanting a global counter. We can implement this by using the infamous unsafePerformIO
. Capable of doing the unthinkable, it can bypass the type system and execute IO
in a pure context. We will use this to our advantage, allowing us to create an IORef
at the top level.
unsafePerformIO :: IO a -> a
“unsafePerformIO can be extremely useful”
– Simon Peyton-Jones 2001
While it is extremely useful, as the name suggests it is highly unsafe and will require some special care as we will see.
IORef
is thread-safe when used correctly, and is an efficient way to share state between threads both globally and in more restrained settings.
For our counter we first need to create a global IORef Int
to store the count. This is where care must be taken as we need to make sure our variable is not inlined or it may accidentally be created multiple times when optimizations are enabled.
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
counterRef :: IORef Int
counterRef =
unsafePerformIO (newIORef 0)
{-# NOINLINE counterRef #-}
Finally we need to create a function to increment our counter. Given our counter can be modified from anywhere, it’s a good practice to make sure our modifications to the count are thread-safe. We can update an IORef
safely from any thread using atomicModifyIORef
.
atomicModifyIORef :: IORef a -> (a -> (a, b)) -> IO b
atomicModifyIORef
takes an IORef
and an update function which allows us to give a new value for the reference while returning also something useful to the caller. We use the strict variant denoted by the tick '
on the end, to make sure our increments don’t cause a space leak.
incrementCount :: IO Int
incrementCount =
atomicModifyIORef' counterRef (\count -> (count + 1, count + 1))
For completeness we also want to provide a way to get the count without incrementing it, and a way to reset the counter.
readCount :: IO Int
readCount =
readIORef counterRef
resetCount :: IO ()
resetCount =
atomicModifyIORef' counterRef (\_ -> (0, ()))
Now we have a global thread-safe counter that can be accessed from anywhere in just a handful of lines. You can use the same trick to build a global cache using Data.Map
but that’s a story for another time.
One pitfall of this approach to be aware of if you use GHCi is that the counter will not necessarily be reset when you call :reload
. The IORef
is conceptually created when the module it is defined in is loaded. So if the module with the counter has not changed, a reload will not cause the counter to be reset. This can be a bug or a feature depending on your use case.
unsafePerformIO
is indeed extremely useful, but use it with care!
“unsafePerformIO is a dangerous weapon, and I advise you against using it extensively.”
– Simon Peyton-Jones 2001
Complete Code
import Data.IORef
import System.IO.Unsafe (unsafePerformIO)
counterRef :: IORef Int
counterRef =
unsafePerformIO (newIORef 0)
{-# NOINLINE counterRef #-}
incrementCount :: IO Int
incrementCount =
atomicModifyIORef' counterRef (\count -> (count + 1, count + 1))
readCount :: IO Int
readCount =
readIORef counterRef
resetCount :: IO ()
resetCount =
atomicModifyIORef' counterRef (\_ -> (0, ()))
Credits
Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. Simon Peyton Jones (2001)
Global variables in Haskell. John Hughes (2004)
Photo by Juliana Kozoski on Unsplash