The H2 Wiki


fork-fragile-reader-like-operations

Fork-fragile reader-like operations in Haskell

– Tom Ellis, June 2026

The Haskell ecosystem contains several examples of “reader-like” operations that run in IO rather than in a specific “reader-like” monad. Such operations necessarily have fragile behaviour when performed in a forked thread, because Haskell does not yet have suitable primitives with which to implement such operations robustly. (For more information see Haskell’s missing mutable reference type). This article catalogues some examples.

Reader-like operations in IO

The reader-like operations in question are, firstly, an operation to read an ambient state (analogous to ReaderT’s ask, and with a type like IO a) and, secondly, an operation to locally modify the ambient state (analogous to ReaderT’s local, and with a higher order type like IO a -> IO a). There are two approaches one can take to implement such operations.

  1. Store the ambient state in a mutable reference type, shared between all operations that interact with it. “Asking” for the current value of the ambient state amounts to reading the current value of the reference; “locally modifying” the ambient state amounts to updating the value of the reference when entering the local block and restoring the original value when leaving the local block.

This is an example of implementation strategy 1:

ambientState :: IORef StateType
ambientState = unsafePerformIO (newIORef initialValue)

ask :: IO StateType
ask = readIORef ambientState

local :: (StateType -> StateType) -> IO StateType -> IO StateType
local f body = do
  -- Read the original value of the state
  orig <- readIORef ambientState
  bracket_
    -- Modify the state to its new value
    (modifyIORef f ambientState)
    -- Restore the original value of the state
    (writeIORef ambientState orig)
    body

Implementation strategy 1 leads to a particular type of fork-fragility, which I’ll call “Symptom 1”.

For example:

concurrently_
  ( do
      ...
      local f $ do
        ...
      ...
  )
  ( do
      ...
      -- ask may see modification due
      -- to `local f` in sibling thread
      s <- ask
      ...
  )

(Haskell’s missing mutable reference type gives another example that exhibits Symptom 1.)

The second approach to implement reader-like operations in IO is:

  1. Store the ambient state in a mutable reference type, shared between all operations that interact with it within a given thread. “Asking” for the current value of the ambient state amounts to reading the current value of that thread’s reference; “locally modifying” the ambient state amounts to updating the value of the that thread’s reference.

This is an example of implementation strategy 2:

-- An ambient state *for each thread*
ambientState :: IORef (Map ThreadId StateType)
ambientState = unsafePerformIO (newIORef initialValue)

ask :: IO StateType
ask = do
  m <- readIORef ambientState
  t <- myThreadId
  -- Look up the value of *this thread's* ambient state
  pure (Map.lookup t m)

local :: (StateType -> StateType) -> IO StateType -> IO StateType
local f body = do
  -- Read the original value of the state
  orig <- readIORef ambientState
  t <- myThreadId
  bracket_
    -- Modify the state to its new value, for *this thread* only
    (modifyIORef (Map.adjust f t) ambientState)
    -- Restore the original value of the state
    (writeIORef ambientState orig)
    body

Implementation strategy 2 leads to a different type of fork-fragility, which I’ll call “Symptom 2”.

For example:

local g $ do
  forkIO $ do
    -- ask does not see the value
    -- of the state set by g
    s <- ask
    ...
  ...

A potential solution

To implement reader-like operations in IO that are subject to neither symptom, Haskell needs a new feature. A hypothetical such feature is described in these articles:

In the absence of such a feature, reader-like operations in IO will continue to exhibit fork-fragile behaviour. This article concludes with a catalogue of some ecosystem examples of reader-like operations which, inevitably, exhibit such behaviour.

Catalogue of fork-fragile reader-like operations