# You don't need an effect system

**URL:** <https://discourse.haskell.org/t/you-dont-need-an-effect-system/14792>\
**Category:** Links\
**Created:** [October 3, 2026, 6:26pm UTC](https://discourse.haskell.org/t/you-dont-need-an-effect-system/14792 "2026-10-03T18:26:26Z")\
**Posts on this page:** 1\
**Showing post:** 9

<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:** [October 4, 2026, 2:35pm UTC](https://discourse.haskell.org/t/you-dont-need-an-effect-system/14792/9 "2026-10-04T14:35:12Z")

</div>

I broadly agree! This is basically `RIO`/`ReaderT _ IO` minus the (reader) monad bit (i.e. just passing stuff around, nice) plus the attempt at scoped exceptions (nice). I think most Haskell users would be better served by that style than by traditional (“synthetic”) effect systems. However, I think “analytic” effect systems (first `effectful`, later Bluefin) which are wrappers around `IO` (in Bluefin’s case, a very simple wrapper around `IO`) are a compelling alternative.

There are some caveats with the article, one serious. The serious one, a deal breaker for me, and probably a dealbreaker for all pratical use cases, is that `Early e` aliases. If I have two `Early e`s floating around, with the same `e`, they interfere with each other. Consider this:

```haskell
-- > aliasExample
-- "inner caught: tried to leave via outer"
aliasExample :: IO String
aliasExample = do
  result <- runEarly $ \outer -> do
    let act = leave outer "tried to leave via outer"
    innerResult <- runEarly $ \_inner -> do
      act
    case innerResult of
      Left err -> pure ("inner caught: " ++ err)
      Right impossible -> absurd impossible
  pure $ case result of
    Left err -> "outer caught: " ++ err
    Right message -> message

```

`act` can be arbitrarily complicated and defined arbitrarily far away from its use within the inner `runEarly`, but there’s nothing the caller can do to ensure that its exception gets picked up by the intended handler (i.e. the outer `runEarly`).

The particularly dangerous case is where you have a function `f` uses `runEarly` at type `e` and that accepts a `g :: ... -> IO r` as an argument. If you call `f` with a `g` that has another `Early e` in its closure then a call of `leave` in `g` will not return to the point you expect.

That’s already solved though, by [`Bluefin.Internal.Exception.Scoped`](https://github.com/tomjaguarpaw/bluefin/blob/012aa5539e6313b84caddf80c590a122f81ac7ec/bluefin-internal/src/Bluefin/Internal/Exception/Scoped.hs) (based on an ideas from Andrzej Rybczak and @Leary). It’s a simple drop-in replacement for `Early` defined in the article.

A second caveat, less serious and not a dealbreaker, is that `Early`s can escape their scope:

```haskell
-- > escapeExample
-- *** Exception: Early return exception
escapeExample :: IO ()
escapeExample = do
  innerResult <- runEarly pure
  inner <- case innerResult of
    Left () -> error "Impossible"
    Right early -> pure early

  escapedEarly <- runEarly $ \outer ->
    leave outer inner
  case escapedEarly of
    Left inner ->
      leave inner ()
    Right impossible -> absurd impossible

```

It might be nice to prevent that from happening. How could we do that? Well, one could use scope tracking [a la Bluefin](https://hackage-content.haskell.org/package/bluefin-0.7.0.0/docs/Bluefin-Capability-Throw.html#t:Throw). That comes with some, but I would claim not much, additional complexity. I won’t hold it against anyone who doesn’t want to pay that additional complexity.

* * *

Beyond that, there were a number of claims in the article that don’t make sense to me, and some other things I thought worth commenting on:

> Does it make for a safer codebase?
> 
> No, tracking effects does not preclude anyone from adding bad IO to an effect implementation, from importing unsafePerformIO, or from finding a sum of an infinite list

Perhaps not, but then by the same logic Haskell doesn’t make for a safer codebase than Python.

> Am I using it outside of IO?
> 
> No, for pure functions that do mutation the ST monad works just fine.

For pure functions that do mutation _and_ throw exceptions the ST monad doesn’t work fine. For that you need Bluefin.

> random number generation is commonplace, but the steps necessary to generate a random value are different every time. The only shared behavior is the use of generator state, and even then it’s unclear if passing it around has any benefits over using the global generator

Passing generators around has a performance benefit over the global generator, if one is in a concurrent situation: “[AtomicGenM is the slowest of the monadic adapters due to the overhead of its atomic operations](https://hackage-content.haskell.org/package/random-1.3.1/docs/System-Random-Stateful.html#t:AtomicGenM)”.

> Have I made use of higher-order effects?
> 
> No, none of the effects I’m using need to overlap or nest

There seems to be a misconception here. The effects from this article are perfectly consistent with higher order uses, and here _is_ a higher-order effect from later in the article:

```haskell
runStack
  :: Logger -> Metrics -> DatabasePool -> MessagingEnv
  -> (Early () -> Database -> Messaging -> IO ())
  -> IO ()

```

The presence of something returning `IO` in argument position makes `runStack` a higher order effect.

(I’m not sure what the article means by “nest”. The effects in this article clearly nest! And higher order effects are not really about overlapping or nesting.)

> This may well be the most embarrassing problem in all of Haskell. Over and over again people waltz in with the exact same basic question: “How do I return from a function early?”.

I agree it’s embarrassing. To dispute nomenclature, `runEarly` is not really an “early return”, it’s more like a “scoped try”. “Early return” would be something like `Typeable e => (Early e -> IO e) -> IO e`.

(For the record, the [GHC proposal linked as a footnote](https://github.com/ghc-proposals/ghc-proposals/issues/503) was one of the things that led me to develop Bluefin, since I did consider it embarrassing that early return was considered difficult enough to require a syntax extension.)

> And just like that I ran out of reasons to use an effect system.

`IO` doesn’t have a “thread-safe, locally modifiable state” so if one wants that one has to use an effect system (although there is a proposal to add thread-safe, locally modifiable state to `IO`, in the [scoped thread-locals proposal](https://github.com/ghc-proposals/ghc-proposals/pull/751)). (Perhaps @BurningWitness really [does not need that](https://discourse.haskell.org/t/haskells-missing-mutable-reference-type/14248/8), though).

> There is only one unique feature effect systems—or, more generally, custom monads designed to supercede IO—provide: they can do anything in between the lines [of code].

Hmm, no, not really. Bluefin doesn’t do anything “between the lines” (with the arguable exception of passing around a `Vault` to support the [`Ask`](https://hackage-content.haskell.org/package/bluefin-0.6.0.0/docs/Bluefin-Capability-Ask.html) capability, but if `IOScopedRef` is added to GHC then it won’t even need to do that). Bluefin effectful operations accept value-level capabilities as arguments.

Compared to the style presented in the article, the benefits are Bluefin are

- No leaked effects (such as the leaked exceptions above), no leaked resource handles (no “`Database`” is in scope after the connection is closed)
- You can run observationally-pure code to a pure value, i.e. you have `runPureEff :: (forall e. Eff es r) -> r`

The relative costs are:

- You write `e1 <: es => Throw e e1 -> Eff es r` rather than `Early e -> IO r`
- You sometimes have to manually munge these `e1`/`es` thinks with `useImpl` and friends.

(I’ll let the authors of other effect systems speak for themselves if they object to that characterization, but the others are all significantly more complex than Bluefin.)

> Effects are tracked as function arguments; this is quite different from effect systems, which prefer constraints

No, not in the case of Bluefin. It also tracks effects as function arguments.

> Here are the disadvantages:
> 
> - Some functions will be verbose.
> 
> I hope this knowledge is used for things.

Strong agreement with the author in this point of style. After using arguments-as-effects in Bluefin for 2.5 years I have never once thought “passing arguments is tedious; I wish it would happen automatically”.

> I’ll use the database effect as an example. Here’s a rough outline of the implementation function:
> 
> ```haskell
> runDatabase connInfo f =
> mask $ \unmask -> do
> ...
> 
> ```

I don’t understand why this isn’t just using `bracket`. Isn’t that one of the advantages of this style, that you can use `bracket` as it is, without any wrapping? Have I missed something? I also don’t understand why the implemenation throws a naked `DatabaseException`:

> ```haskell
> case fromException ex of
> Just dbEx -> pure $ Left (dbEx :: DatabaseException)
> Nothing -> _ -- (4) Failure above.
> 
> ```

Shouldn’t it take advantage of `Early DatabaseException`?

---

_[View the full topic](https://discourse.haskell.org/t/you-dont-need-an-effect-system/14792)._
