The H2 Wiki


fork-fragile-reader-like-operations

Fork-fragile reader-like operations in Haskell

– Tom Ellis, June 2026


This article is part of a collection


Introduction

The Haskell ecosystem contains several examples of “reader-like” operations that run in IO rather than in a specific “reader-like” monad. They 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)
{-# NOINLINE ambientState #-}

ask :: IO StateType
ask = readIORef ambientState

local :: (StateType -> StateType) -> IO r -> IO r
local f body = do
  -- Read the original value of the state
  orig <- readIORef ambientState
  bracket_
    -- Modify the state to its new value
    (modifyIORef ambientState f)
    -- 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 Map.empty)
{-# NOINLINE ambientState #-}

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

local :: (StateType -> StateType) -> IO r -> IO r
local f body = do
  -- Read the original value of the state
  m <- readIORef ambientState
  t <- myThreadId
  let orig = fromJust (Map.lookup t m)

  bracket_
    -- Modify the state to its new value,
    -- for *this thread* only
    ( atomicModifyIORef' ambientState $ \m' ->
        (Map.insert t (f orig) m', ())
    )
    -- Restore the original value of the state,
    -- for *this thread* only
    ( atomicModifyIORef' ambientState $ \m' ->
          (Map.insert t orig m', ())
    )
    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, except in the special case of masking operations, inevitably exhibit such behaviour.

Masking: fork-resilient reader-like operations

Control.Exception has mask_, uninterruptibleMask_ and interruptible (each of type IO a -> IO a), reader-like operations which, in effect, locally modify a value of type MaskingState within their body. Local modifications to the masking state in one thread are not observed within other threads but the masking state is inherited by child threads, so these operations are not fork-fragile. How? They have a special implementation in GHC’s RTS. Unfortunately, that implementation cannot currently be used as a way to obtain user-defined local states.

Catalogue of fork-fragile reader-like operations