# \[ANN\] memo-io (replacing thread on GHC proposal for top-level IO)

**URL:** <https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328>\
**Category:** Announcements\
**Created:** [June 29, 2026, 5:09am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328 "2026-06-29T05:09:20Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![brandonchinn178](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/brandonchinn178/32/5103_2.png) [@brandonchinn178](https://discourse.haskell.org/u/brandonchinn178)\
**Post date:** [June 29, 2026, 5:09am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/1 "2026-06-29T05:09:20Z")

</div>

> <https://github.com/ghc-proposals/ghc-proposals/pull/766>
>
> \[Rendered\](https://github.com/brandonchinn178/ghc-proposals/blob/top-level-io/pr…oposals/0766-shared-io.rst)

This has been a long-standing discussion, as documented at the [Top-level mutable state](https://wiki.haskell.org/index.php?oldid=64612) page in the Haskell Wiki. After discussion in [Can NOINLINE fail to prevent inlining?](https://discourse.haskell.org/t/can-noinline-fail-to-prevent-inlining/14324), this is my attempt at the problem.

The core of my proposal is:

- `OPAQUE` is better than `NOINLINE` for this idiom, but it’s not obvious that one should use `OPAQUE` over `NOINLINE`
- One must use `unsafePerformIO`, but it’s not actually unsafe if one uses `OPAQUE`. You lose ordering guarantees with regard to other IO actions, but that’s no more unsafe than `hGetContents`.
- `foo :: %TopLevelIO (IORef Int); foo = newIORef 0` is actually semantically sound and safe, provided the compiler respect the modifier

---

<div class="post-metadata">

**Author:** ![jaror](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/jaror/32/3271_2.png) [@jaror](https://discourse.haskell.org/u/jaror)\
**Post date:** [June 29, 2026, 7:29am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/2 "2026-06-29T07:29:14Z")

</div>

Can you give an example where CSE happens?

---

<div class="post-metadata">

**Author:** ![BurningWitness](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/burningwitness/32/3665_2.png) [@BurningWitness](https://discourse.haskell.org/u/BurningWitness)\
**Post date:** [June 29, 2026, 7:45am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/3 "2026-06-29T07:45:27Z")

</div>

> [@brandonchinn178](#):
>
> Haskell programmers occasionally need a genuinely top-level, mutable, effectfully-initialized value

Can you think of any example of this that couldn’t be solved with thread-local state instead? (e.g. [Haskell's missing mutable reference type - #8 by BurningWitness](https://discourse.haskell.org/t/haskells-missing-mutable-reference-type/14248/8))

---

<div class="post-metadata">

**Author:** ![DavidB](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/davidb/32/3616_2.png) [@DavidB](https://discourse.haskell.org/u/DavidB)\
**Post date:** [June 29, 2026, 1:18pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/4 "2026-06-29T13:18:47Z")

</div>

Here is how your proposal could be modified to be more in line with the `ACIO` proposal from [Top level mutable state - HaskellWiki](https://wiki.haskell.org/index.php?oldid=64612) :

Add the following module to base:

```haskell
{-# LANGUAGE StandaloneKindSignatures #-}
module Control.Monad.ACIO
  ( ACIO (), -- Constructor not exposed.
    acioToIO,
    newIORef,
    getNumProcessors,
  )
where

import Control.Monad.IO.Class
import qualified Data.IORef as IO
import Data.Kind (Type)
import qualified GHC.Conc as IO

-- | IO-like monad with the the following two properties:
-- - The effect of `ACIO a` can be omitted if the result `a` is not needed in the program.
-- - The effect of running `ACIO a` can be commuted with any other effect in IO.
type ACIO :: Type -> Type
newtype ACIO a = MkACIO (IO a)

instance Functor ACIO
instance Applicative ACIO
instance Monad ACIO

acioToIO :: ACIO a -> IO a
acioToIO (MkACIO x) = x

newIORef :: a -> ACIO (IO.IORef a)
newIORef x = MkACIO $ IO.newIORef x

getNumProcessors :: ACIO Int
getNumProcessors = MkACIO IO.getNumProcessors

-- Extendable with new operations that are verified to fulfil the promises of ACIO

```

Then desugar:

```haskell
f :: %ToplevelIO a
f = body

```

(where `body :: ACIO a`) to

```haskell
{-# OPAQUE f #-}
f :: a
f = unsafePerformIO (acioToIO body)

```

This would preserve the reasoning principles of safe code, also wouldn’t special case the `newIORef` method, and make it extensible by just adding new functions to the `Control.Monad.ACIO` module which have been verified to fulfil the semantic properties of that monad.

You wouldn’t be able to run arbitrary IO code or extend the monad with new methods in user libraries, except by using `unsafeCoerce`. But that is probably what you want, since it is not semantically sound to lift arbitrary IO functions to ACIO.

---

<div class="post-metadata">

**Author:** ![brandonchinn178](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/brandonchinn178/32/5103_2.png) [@brandonchinn178](https://discourse.haskell.org/u/brandonchinn178)\
**Post date:** [June 29, 2026, 5:26pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/5 "2026-06-29T17:26:42Z")

</div>

My original motivation is for prometheus metrics backed by a ByteArray. IIUC your suggestion would be equivalent to a global Reader effect (with `local`), whereas I intentionally want a global State/Writer effect

---

<div class="post-metadata">

**Author:** ![brandonchinn178](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/brandonchinn178/32/5103_2.png) [@brandonchinn178](https://discourse.haskell.org/u/brandonchinn178)\
**Post date:** [June 29, 2026, 5:31pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/6 "2026-06-29T17:31:15Z")

</div>

1. There’s no special casing of newIORef. It allows any IO
2. Your ACIO code snippet has a MonadIO instance, so anyone could liftIO arbitrary actions in it
3. You can’t liftIO an ACIO action, liftIO only works on IO. You need to export an explicit `acioToIO` function
4. I’m not sure why we need to restrict IO actions. Where is the unsafety here? If you’re worried about third-party code running arbitrary IO code in an innocuously named pure value, you can already do so with unsafePerformIO today. Making it easier/safer to use doesn’t change the attack vector

---

<div class="post-metadata">

**Author:** ![BurningWitness](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/burningwitness/32/3665_2.png) [@BurningWitness](https://discourse.haskell.org/u/BurningWitness)\
**Post date:** [June 29, 2026, 5:50pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/7 "2026-06-29T17:50:24Z")

</div>

And a `State`/`Writer` effect is the same as passing a mutable value as  
a `Reader` effect/plain argument.

I’m not fond of effect system language in this context, it obscures implementation. The point is simply that thread-local state is not something that can be inlined, so if it were a feature you’d be able to use it to sidestep the entire problem.

---

<div class="post-metadata">

**Author:** ![DavidB](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/davidb/32/3616_2.png) [@DavidB](https://discourse.haskell.org/u/DavidB)\
**Post date:** [June 29, 2026, 5:53pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/8 "2026-06-29T17:53:43Z")

</div>

> 1. There’s no special casing of newIORef. It allows any IO

Sorry, I expressed myself a bit awkwardly. Previous proposals (not yours) proposed to special case `newIORef`. Both your original proposal and my proposed variant using `ACIO` don’t special case `newIORef`.

> 1. Your ACIO code snippet has a MonadIO instance, so anyone could liftIO arbitrary actions in it
> 2. You can’t liftIO an ACIO action, liftIO only works on IO. You need to export an explicit `acioToIO` function

Sorry, didn’t check my code carefully enough. I have changed the snippet and the desugaring accordingly. (Removed the MonadIO instance and added `acioToIO`.)

> 1. I’m not sure why we need to restrict IO actions. Where is the unsafety here? If you’re worried about third-party code running arbitrary IO code in an innocuously named pure value, you can already do so with unsafePerformIO today. Making it easier/safer to use doesn’t change the attack vector

It’s not about an attack vector by a malicious actor, but about semantic guarantees.  
If you allow to run arbitrary IO actions with `unsafePerformIO` using the on-demand semantics that you propose, then you potentially invalidate valid Haskell rewrites. I gave an example in the GitHub discussion. Let me repost it here:

```haskell
f :: %ToplevelIO (IORef String)
f = newIORef "one"

g :: %ToplevelIO ()
g = writeIORef f "two"

main :: IO ()
main = do
  case g of
    () -> do
      x <- readIORef f
      print x -- prints "two"

-- After "refactoring" `main` by removing redundant case expression on unit type
main' :: IO ()
main' = do
  x <- readIORef f
  print x -- prints "one"

```

The difference with the restricted set of actions living in `ACIO` is that they do _not_ invalidate any Haskell reasoning if performed using `unsafePerformIO`. That is why previous proposals suggested just special casing `newIORef` on the toplevel.

An alternative is to not have a lazy on-demand semantics of toplevel IO, but to give a deterministic “initialization” semantics to toplevel IO: They are all run in some deterministic order upon program start.

---

<div class="post-metadata">

**Author:** ![DavidB](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/davidb/32/3616_2.png) [@DavidB](https://discourse.haskell.org/u/DavidB)\
**Post date:** [June 29, 2026, 6:48pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/9 "2026-06-29T18:48:12Z")

</div>

(I was also turning crazy because I knew that this had been discussed somewhere, but I couldn’t find it in the GHC proposals repo. I finally found it linked from the old Haskell prime mailing list from around 2008: [Re: [Haskell-cafe] global variables - Haskell-prime - Haskell.org](https://mailman.haskell.org/archives/list/haskell-prime@haskell.org/thread/QIP4C2XWULNON27IDAQ3WZHSYWITLC3H/) )

---

<div class="post-metadata">

**Author:** ![olf](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/olf/32/3813_2.png) [@olf](https://discourse.haskell.org/u/olf)\
**Post date:** [June 29, 2026, 7:26pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/10 "2026-06-29T19:26:33Z")

</div>

> [@DavidB](#):
>
> `-- - The effect of `ACIO a` can be omitted if the result `a` is not needed in the program.`

> [@DavidB](#):
>
> An alternative is to not have a lazy on-demand semantics of toplevel IO,

Just out of curiosity: If you knew whether the result of an action is unused, you could solve the Halting Problem, couldn’t you? In other words, the property we’re looking for is `acio >>= (const (return a)) = return a` which holds for a surprisingly small family of monads.

---

<div class="post-metadata">

**Author:** ![brandonchinn178](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/brandonchinn178/32/5103_2.png) [@brandonchinn178](https://discourse.haskell.org/u/brandonchinn178)\
**Post date:** [June 29, 2026, 8:38pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/11 "2026-06-29T20:38:37Z")

</div>

I agree with Adrian’s point here:

> So pragmas that influence optimisation are something I can live with. But using pragmas to influence \*semantics\* really is an evil practice IMO and is something that should be discouraged, not made an unavoidable necessity.

I don’t like the fact that we’re relying on a pragma here for semantics. Unfortunately, we already have this in the `OVERLAPPING` pragmas, but I think we should move away from this.

---

<div class="post-metadata">

**Author:** ![metaml](https://avatars.discourse-cdn.com/v4/letter/m/74df32/32.png) [@metaml](https://discourse.haskell.org/u/metaml)\
**Post date:** [June 29, 2026, 9:06pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/12 "2026-06-29T21:06:03Z")

</div>

Global variables seems to be many steps backwards for Haskell. If I had a say, I vote to abandon this and redirect efforts on fixing bugs or supporting abandoned hackage packages.

There was a time, when a good number of GHC proposals had clear denotational and operational semantics fleshed out as well all the concommitant proofs on completeness, soundness, etc. It would be nice to see something similar instead of this ad-hoc coding approach that affects all GHC programmers.

Couldn’t this be done as a library instead of a sledge-hammer approach of modifying the compiler?

I’m almost postive that many–if not the majority–of haskell programmers would find “global varaibles” an awfully bad idea at the compiler level.

---

<div class="post-metadata">

**Author:** ![tomjaguarpaw](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/tomjaguarpaw/32/1230_2.png) [@tomjaguarpaw](https://discourse.haskell.org/u/tomjaguarpaw)\
**Post date:** [June 29, 2026, 9:27pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/13 "2026-06-29T21:27:58Z")

</div>

> [@brandonchinn178](#):
>
> I intentionally want a global State/Writer effect

State and Writer are very different (the former is difficult to use concurrently, for example). Is Writer really enough?

---

<div class="post-metadata">

**Author:** ![DavidB](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/davidb/32/3616_2.png) [@DavidB](https://discourse.haskell.org/u/DavidB)\
**Post date:** [June 29, 2026, 9:28pm UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/14 "2026-06-29T21:28:04Z")

</div>

Yes, I agree with the principle that using pragmas should not influence semantics. But this sentiment holds, in my opinion, only for code that doesn’t `unsafeX` functions. Using `unsafeX` means that you are leaving the realm of Haskell semantics and entering the realm of GHC implementation details. And also that the semantics of the code might be dependent on details of the optimization pipeline that are influenced by pragmas.

The other principle I would like to propose is that enabling a language extension should not change the equational theory of Haskell. I don’t think there are any language extensions that do this (maybe `Strict`, `StrictData` do this at the moment?). If you restrict the proposal to `ACIO` then you can make a reasonably solid argument that you don’t change the equational theory, but if you allow any top level IO then you substantially weaken it.

> [@olf](#):
>
> Just out of curiosity: If you knew whether the result of an action is unused, you could solve the Halting Problem, couldn’t you? In other words, the property we’re looking for is `acio >>= (const (return a)) = return a` which holds for a surprisingly small family of monads.

A monadic action `f :: IO a` has the first property if `f >> g = g` for all IO actions `g`. This is true for almost all IO actions which don’t change the environment but read something from it. So most `getX` functions from `System.Environment`, but also `readIORef`, or functions that generate a random number in the IO monad. Importantly, `newIORef` also has this property.

Very few monadic functions `f` have the second property, i.e. they commute with **all** other IO actions. In particular, this means they cannot have any observable effect in time, or which can be influenced by other IO actions. `readIORef` doesn’t have this property since it does not commute with `writeIORef`, for example. `newIORef` is the main example of a function having this property, but `getNumProcessors` similarly commutes with all other IO actions, since no IO action can change the number of CPU cores. `getRandom` specialized to IO also has the property, if we consider that the state of the PRNG is not observable.

The main problem also is not so much decidability but that we currently don’t have a formal model of the semantics of IO. So we cannot really say when to IO actions `f` and `g` are equivalent. Clearly we have that `print "hello" >> print "world" != print "world" >> print "hello"`, but as soon as we have `IORef`s, `forkIO` etc. it all becomes muddy in the absence of a formal memory model.

---

<div class="post-metadata">

**Author:** ![metaml](https://avatars.discourse-cdn.com/v4/letter/m/74df32/32.png) [@metaml](https://discourse.haskell.org/u/metaml)\
**Post date:** [June 30, 2026, 3:23am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/15 "2026-06-30T03:23:31Z")

</div>

fwiw: “Functional Pearl Global variables in Haskell” by John Hughes.

---

<div class="post-metadata">

**Author:** ![olf](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/olf/32/3813_2.png) [@olf](https://discourse.haskell.org/u/olf)\
**Post date:** [June 30, 2026, 9:26am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/16 "2026-06-30T09:26:19Z")

</div>

> [@DavidB](#):
>
> if `f >> g = g` for all IO actions `g`.

> [@DavidB](#):
>
> This is true for almost all IO actions which don’t change the environment

I disagree. Doesn’t `newIORef` allocate something and alter the heap? Couldn’t the host system of a VM alter the result of `getNumProcessors` over the lifetime of the Haskell program?

The `>>` example is only the most obvious, syntactic case. My criticism is that no compiler can, in general, tell whether `subsequentCode :: IORef a -> IO something` is a constant function and therefore the preceding call to `newIORef` can be optimized away or floated elsewhere in the control flow.

---

<div class="post-metadata">

**Author:** ![DavidB](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/davidb/32/3616_2.png) [@DavidB](https://discourse.haskell.org/u/DavidB)\
**Post date:** [June 30, 2026, 10:46am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/17 "2026-06-30T10:46:00Z")

</div>

> [@olf](#):
>
> I disagree. Doesn’t `newIORef` allocate something and alter the heap? Couldn’t the host system of a VM alter the result of `getNumProcessors` over the lifetime of the Haskell program?

Operationally `newIORef` does alter the state of the heap. But this change shouldn’t be observable from Haskell programs according to any sensible semantics for `IO` and `IORefs`. The purpose of a semantics of IO is to give exact definitions of what IO programs are considered equivalent and should always allow to optimize `newIORef x >> m` to `m`.  
(Section 4 of the SC-Haskell paper [https://www.recurial.com/ppopp17-sc-haskell.pdf](https://www.recurial.com/ppopp17-sc-haskell.pdf) gives a good idea of how such a semantics for IORefs should look like. They specify sequential consistency for IORefs, but weaker memory models are possible as well.)

> [@olf](#):
>
> The `>>` example is only the most obvious, syntactic case. My criticism is that no compiler can, in general, tell whether `subsequentCode :: IORef a -> IO something` is a constant function and therefore the preceding call to `newIORef` can be optimized away or floated elsewhere in the control flow.

For this use case it is not essential that the compiler can decide whether a value is used. The property is needed to make `%ToplevelIO` sound. Especially for the combination of `lazyness` + `unsafePerformIO`: `lazyness` means that the IO effect of a toplevel declaration `f :: %ToplevelIO` will not be executed if the result is not needed and that it may be performed at an unpredictable point in time. The first property of `ACIO` guarantees that the semantics of the program doesn’t change if the `f` is not executed at all, and the second property of `ACIO` guarantees that the semantics of the program doesn’t depend on the point in time that the IO effect of `f` occurs.

---

<div class="post-metadata">

**Author:** ![olf](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/olf/32/3813_2.png) [@olf](https://discourse.haskell.org/u/olf)\
**Post date:** [June 30, 2026, 11:24am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/18 "2026-06-30T11:24:00Z")

</div>

Thanks for the explanation. So the burden of proof of constant-ness is changed from the function to the argument. Clever!

---

<div class="post-metadata">

**Author:** ![brandonchinn178](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/brandonchinn178/32/5103_2.png) [@brandonchinn178](https://discourse.haskell.org/u/brandonchinn178)\
**Post date:** [July 1, 2026, 4:07am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/19 "2026-07-01T04:07:07Z")

</div>

Updated the proposal! Now it’s closer to the ACIO proposal

---

<div class="post-metadata">

**Author:** ![Ambrose](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.haskell.org/ambrose/32/5672_2.png) [@Ambrose](https://discourse.haskell.org/u/Ambrose)\
**Post date:** [July 1, 2026, 7:00am UTC](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328/20 "2026-07-01T07:00:14Z")

</div>

What’s wrong with the existing way to do it? The proposal doesn’t _actually_ explain this.

This proposal smells very superficial to me. We are adding syntax for a very niche pattern. A pattern that offends the core language culture. A pattern that already is 100% supported with some skill.

Because..some spec from the 2010s said we need global state?

I’m sorry but that i read a NOINLINE vs OPAQUE discourse thread and then see a ghc proposal about it is wild to me LOL

[Next page](https://discourse.haskell.org/t/ann-memo-io-replacing-thread-on-ghc-proposal-for-top-level-io/14328.md?page=2)
