alt.hn

5/26/2026 at 5:38:51 PM

Algebraic Effects for the Rest of Us

https://overreacted.io/algebraic-effects-for-the-rest-of-us/

by satvikpendem

5/30/2026 at 8:08:12 AM

I was completely baffled by "algebraic effects" for years. They looked far too confusing for me to want to spend my time on them, and took the "Don’t feel like you have to [get curious about them]" approach.

But then at some point it struck me: underlying all these effect systems is just passing stuff in. So I developed my own effect system for Haskell, Bluefin[1], based on capabilities, which means the "capability to perform some effect" is represented by just passing stuff in (that is, a function can do some effect as long as it has been passed the capability to do it).

From this point of view it's hard to understand the excitement over "resume with" and "the part you can’t do with try / catch. It lets us jump back to where we performed the effect, and pass something back to it from the handler". Programming languages have had that feature since forever: a "resumable exception" is a "function call". A dynamically chosen "resumable exception" is the call of a dynamically chosen function, i.e. the argument to a higher order function.

So I don't know why people love the complexity around "algebraic effects". Maybe the mystique has a certain allure. But if you want the most straightforward possible approach I can recommend you try out Bluefin. I'm happy to answer questions on the issue tracker[2].

(Caveat: Bluefin is able to simplify things dramatically by dropping support for "multi-shot" continuations. But mostly you don't want multi-shot continuations.)

EDIT: I was too pessimistic, bazoom42 has noticed this :) https://news.ycombinator.com/item?id=48334067

[1] https://hackage.haskell.org/package/bluefin

[2] https://github.com/tomjaguarpaw/bluefin/issues/new

by tome

5/30/2026 at 9:35:32 AM

A real effect system allows you to do things like NOT continue execution after using the effect (like the error effect does - if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra steps) or only continuing it after some asynchronous work happens (the Future effect), or even "continue" execution several times. That just cannot be done with "just passing stuff in". You still don't seem to have understood effects.

by brabel

5/30/2026 at 10:08:33 AM

Thanks for your response. Perhaps I'm missing some fundamental things. Could you help?

> A real effect system allows you to do things like NOT continue execution after using the effect

Right, Bluefin's Request allows you to do that too. For example here is an example of handling the request by continuing or not, depending on what the value yielded to the Request is.

    example :: Either String ()
    example = runPureEff $ try $ \ex -> do
      forEach
        ( \r -> do
            request r True
            request r True
            request r False
            request r True
            request r True
        )
        ( \case
            True -> pure ()
            False -> throw ex "Stopped"
        )
> if you "implement" this by using Exceptions, you're not using effects at all, just using Exceptions with extra steps

Not sure I follow that. Above you can see I used an exception (Bluefin's Throw capability), but I couldn't have used only an exception because that would have aborted unconditionally. What am I missing here, that makes "using Exceptions" "not using effects at all"?

> only continuing it after some asynchronous work happens (the Future effect)

I'm not really sure what "a Future effect" is, but I don't see how it's not something that can be run as a function call, at least in Haskell.

> or even "continue" execution several times

Right, these are the multishot continuations which Bluefin doesn't support. I haven't discovered many particularly compelling use cases for multishot continuations but would be very interested in finding some. The developer of the Kyo effect system for Scala, Flavio Brasil, suggested parsing, with multiple parse results, which makes sense.

I'm also not entirely sure Bluefin couldn't simulate common use cases of multishot continuations with threads, but I haven't thought about it very hard.

> You still don't seem to have understood effects.

Possibly true, and part of my puzzlement! I'm always happy to try to improve my understanding. Can you help me see what I've missed?

by tome

5/30/2026 at 11:43:08 PM

I think the parent may be getting at the continuation aspect of effects? Effect systems make the stack a first class object you can reuse, I think a standard example is implementing a scheduler. I'm not familiar with your Bluefin library so maybe it already handles this:

  effect Sched =
    yield : unit -> unit
    fork  : (unit -> unit) -> unit
  end
  
  let mut run_queue = []
  let enqueue t = run_queue := List.concat run_queue [t]
  
  let dequeue () =
    match run_queue with
    | [] -> ()
    | t :: rest ->
      run_queue := rest;
      t ()

  let rec spawn task =
    handle
      task ()
    with
    | return _ -> dequeue ()
    | yield () k ->
      enqueue (fn () -> resume k ());
      dequeue ()
    | fork f k ->
      enqueue (fn () -> resume k ());
      spawn f

  let run main = spawn main

  let worker name steps =
    let rec loop i =
      if i > steps do ()
      else do
        print $"{name}: step {i}";
        perform yield ();
        loop (i + 1)
      end
    in
    loop 1
  
  let () =
    run (fn () ->
      print "main: starting";
      perform fork (fn () -> worker "A" 3);
      perform fork (fn () -> worker "B" 3);
      print "main: forked workers, now yielding";
      perform yield ();
      print "main: done")
output:

  main: starting
  A: step 1
  B: step 1
  A: step 2
  main: forked workers, now yielding
  B: step 2
  A: step 3
  main: done
  B: step 3

by sweetsocks21

5/31/2026 at 11:05:21 AM

Ah yes, OK, I missed the point that the timeout is applied to the entire continuation, not just the part of the computation until the next await. Bluefin can't currently do that. I think I could make it do that, using the same implementation strategy as awaitYield (fork a thread, communicate through an MVar) but I wonder what the point is, given that Bluefin allows you to run the continuation at most once. Is the use case of "run the continuation in a modified environment (e.g. with a timeout)" really that compelling? Maybe it is! But I don't see it yet.

On the other hand, I don't see any difficulty with implementing a scheduler using Await/Yield. I don't think it needs access to the full continuation.

by tome

5/30/2026 at 9:29:09 AM

Equating "algebraic effects" with "continuations" is like saying "if" is just "goto" (which isn't even true, e.g., an if can turn into a cmov or whatever).

The only mystique around algebraic effects is the same mystique there is around monads. I don't know if people have started equating algebraic effects to burritos yet but that's a pretty good way to take something simple and turn it into something confusing.

by sirwhinesalot

5/30/2026 at 9:56:33 AM

> Equating "algebraic effects" with "continuations" is like saying "if" is just "goto"

Fair enough. But are you responding to something I said? I didn't make that equation.

> The only mystique around algebraic effects is the same mystique there is around monads. I don't know if people have started equating algebraic effects to burritos yet but that's a pretty good way to take something simple and turn it into something confusing.

Ah, are you saying that fundamentally there isn't really much to algebraic effects and they're much simpler than they're made out to be? If so then it perhaps we agree?

by tome

5/30/2026 at 10:24:24 AM

We disagree that they're just continuations (only one of many possible implementation strategies) but agree they're nothing special ;)

by sirwhinesalot

5/30/2026 at 10:36:16 AM

I don't think I said they're just continuations. In fact I'm trying to make the point that they're mostly just function calls (and I think in my career I've come across one case where I wanted something beyond function calls (for a constraint solver)). There are "multi-shot" continuations (whether you consider that interface or implementation I don't really mind), which have behaviour than function calls can't express, but I don't know of any algebraic effects beyond that.

What do you think algebraic effects are, if they're not continuations?

EDIT: Ah, based on your comment at https://news.ycombinator.com/item?id=48334737 you might say they're a feature of an intermediate language? So you might take a surface language and "compile to an intermediate language of lambda calculus + algebraic effects", without specifying how that intermediate language is implemented (because it may not even be implemented, per se).

by tome

5/30/2026 at 10:51:07 AM

They're really just a protocol. You can implement them in various ways. They will always be some sort of delimited continuations but a "function call" or continuation passing style or anything of the sort does not have to be involved at all.

For example, let us say I don't allow "multi-shot" continuations like in your library, and I'm implementing Algebraic Effects in my own interpreted language.

One way I can implement effects and handlers is to have a handlers get registered in a stack, then when an effect is triggered, save the IP and current stackframe, search for the right handler and jump to it. "resume" then just resets the stackframe, pushes a value into the stack, and sets the saved IP.

(Only saw your edit after posting, sorry, but yes)

by sirwhinesalot

5/30/2026 at 10:58:10 AM

> They're really just a protocol.

Thanks, that clarifies where you're coming from. Is it possible to specify this protocol somehow, by defining an interface for it? Or by extending lambda calculus with the bits it needs?

(Maybe that's what the Koka folks do in their papers, and if so feel free to say, "yeah read their papers").

by tome

5/30/2026 at 11:09:15 AM

I'm thinking a less formally than that. Protocol in a very layman-y "perform is supposed to do this, resume is supposed to do this".

For example, Koka compiles handlers differently depending if they do multi-shot continuations or not. It can do this because all that matters is "perform is supposed to do this, resume is supposed to do this", not what they turn into (same as "if" turning into "cmov" in certain cases). I think it uses a continuation-passing style sort of implementation, but I can't quite remember.

Daan's libhandler implements effects for C in an entirely different manner. It captures the stack much like my example or a stackful coroutine library would.

I'm sure there a formal definitions in both the koka papers and the libhandler paper, but I just skim that stuff ;)

by sirwhinesalot

5/30/2026 at 11:18:48 AM

> Protocol in a very layman-y "perform is supposed to do this, resume is supposed to do this".

OK, but at the very least it has two primitives "perform" and "resume"? And they're supposed to interact in some particular way?

by tome

5/30/2026 at 12:00:00 PM

Yeah, there's three things you're supposed to implement: try/handle, perform, and resume. The names can vary (e.g., perform is often called "raise" or "do"). They have well defined interactions.

I don't actually know what the original paper describing what algebraic effects are supposed to do is, I just know them informally from Koka, Effekt, etc.

by sirwhinesalot

5/30/2026 at 12:30:36 PM

Interesting, then I wonder if anyone has distinguished them from continuation "protocols" such as shift/reset and prompt/control. Thanks!

Bringing it back to my original point, I guess I'd say that if you already have function calls, exceptions and threading built in to the language then you don't need perform/resume except in niche cases (multi-shot continuations being the only case I know of, but I don't even know of many applications of those).

by tome

5/30/2026 at 9:13:58 AM

Continuations (“function calls”) reduce the amount of optimisations you can do around memory - both space wise and computationally

by jacobp100

5/30/2026 at 9:57:01 AM

Not sure I follow what you're trying to say. Can you elaborate?

by tome

5/31/2026 at 7:09:12 PM

> How Is All of This Relevant to React?

> Not that much. You can even say it’s a stretch.

No mention of React's built-in Context? Because this is really similar. Not exact for sure, this particular example wouldn't work for a few reasons, but as a comparison this looks to just be a more generic version of that.

by Izkata

5/30/2026 at 7:30:23 AM

Oh neat, I'm in the second chance pool.

I submitted this because I've been getting really interested in effect systems, especially now that OCaml 5 has a working production quality example they'd been iterating on for years prior. I wanted to see what it'd look like in Rust too so maybe one day we can get rid of async function coloring, and with OxCaml by Jane Street maybe we could see how that would look in practice.

Another reason for submitting this is that React actually has a quite robust effect system, that people don't necessarily realize they're using one every day if they use hooks.

by satvikpendem

5/30/2026 at 9:58:37 AM

(2019)

It’s worth clarifying that for the most part, this article just discusses plain effects, ie. "reified side effects" or "resumable exceptions". Algebraic effects are about the composition (ie. algebra) of effects, exactly like algebraic data types are about the composition of types. This part is generally not meaningful in an untyped language like Javascript because effects are all YOLO and you never know what’s going to happen, what effects a function might throw, or whether there’s any handler up-stack to catch your effect.

by Sharlin

5/30/2026 at 10:16:18 AM

I've never understood what's meant by "algebraic" in that sense, or what this algebra of combining effect is. Could you say more, or maybe share a link to a useful reference?

by tome

5/30/2026 at 3:54:48 PM

Here is an article by Andrej Bauer answering the exact question, "What is algebraic about algebraic effects and handlers?": https://arxiv.org/abs/1807.05923v2

Contrary to the claim from the comment you are replying to, according to Andrej Bauer, "algebraic" does not refer to the composition of different algebraic effects via composing their handlers.

When you see "algebra," think "algebraic structure," i.e., a set of operations and a set of equational laws on those operations.

An algebraic effect consists of a set of effectful operations. In that sense, each algebraic effect (reader, state, etc.) defines its own algebraic structure. In theory, the operations of an effect should also be related to each other by laws. Here is an example for the state effect from Andrej Bauer's paper:

    lookup(ℓ,λs.lookup(ℓ,λt.κst)) = lookup(ℓ,λs.κss)
    lookup(ℓ,λs.update((ℓ,s),κ)) = κ()
    update((ℓ,s),λ_.lookup(ℓ,κ)) = update((ℓ,s),λ_.κs)
    update((ℓ,s),λ_.update((ℓ,t),κ)) = update((ℓ,t),κ)
Therefore, the state effect has an algebraic structure.

However, monads that cannot be defined in this equational style cannot be translated to algebraic effects. An example is the continuation monad: https://old.reddit.com/r/haskell/comments/44q2xr/is_it_possi... (I think you are involved in the Reddit comment chain that I'm linking to...)

AFAIK, another example is the "exception" monad, because the way that you interact with it is through the handler itself. I once saw a thread on r/Haskell discussing this, but can't find it.

by hutao

5/30/2026 at 7:59:16 PM

Ah, thanks, maybe this holds a clue! (Clearly I have been interested in getting to the bottom of this for a while.)

So maybe an "algebraic effect" is one that's isomorphic to a free monad of a functor that itself is an algebraic data type. That seems to give an unambiguous specification for what it means to handle an effect (a natural transformation) and to take a "free product" of effects (sum the functors).

On the other hand I think it would mean that things like Future and general IO wouldn't be algebraic effects.

by tome

5/30/2026 at 11:33:13 PM

This is the gist of Andrej Bauer's paper:

- An algebraic theory consists of a "signature" (specifying a set of operation symbols with their arities) and a set of equations about those operations. An algebraic theory is purely syntactic, and does not give meaning to the operations or equations.

- An interpretation of an algebraic theory maps each operation symbol to a concrete mathematical object. It maps syntax to semantics.

- A model is an interpretation in which the algebraic theory's equations hold.

- A free model of an algebraic theory, generated by a set, is basically a model that can be mapped to any other model of the theory generated by the set.

So, an example of an algebraic theory is a group (as in the algebraic structure itself, not the various instances of a group). Its signature consists of the binary operation of combining, the unary operation of inverse, and the nullary operation of identity. The models of a group are its instances, such as the integers under additions and subtraction, or permutation functions on a set. The integers are the free group (that is, the free model of the group) generated by the set {1}.

Algebraic theories can also describe the effects of a programming language (e.g. reader, writer, or state). The free model of an algebraic effect is given by the syntax trees describing its effectful computations. Effect handlers are maps from syntax trees, transforming computations into other computations within the programming language.

> On the other hand I think it would mean that things like Future and general IO wouldn't be algebraic effects.

For what it's worth, Example 2.3 in the paper states that IO is an algebraic effect, albeit one with no equations. What might be confusing is that the handler of the IO effect cannot have the level of concreteness to actually implement the IO operations. Effect handlers only map computations to computations within the "pure" programming language. From the paper:

> What we still lack is a mathematical model of computational effects at the level of the external environment in which the program runs. There is always a barrier between the program and its external environment, be it a virtual machine, the operating system, or the underlying hardware. The actual computational effects cross the barrier, and cannot be modeled as handlers. A handler gets access to the continuation, but when a real computational effects happens, the continuation is not available. If it were, then after having launched missiles, the program could change its mind, restart the continuation, and establish world peace.

by hutao

5/31/2026 at 9:56:04 AM

> Example 2.3 in the paper states that IO is an algebraic effect

Oh, I meant what Haskell calls `IO`, which includes the ability to launch threads, use delimited continuation primops, abort the program, communicate with the FFI, and all sorts of other things that I would guess don't have an algebraic presentation.

by tome

5/30/2026 at 12:30:59 PM

This is my favorite one, short and understandable: https://philipnilsson.github.io/Badness10k/posts/2017-05-07-...

I show it when I teach Haskell, and it's what usually makes it "click" for students. Probably because motivating examples are in normal imperative pseudocode.

by sodic

5/30/2026 at 1:10:30 PM

Your linked article hints at the advantages of using Monads and therefor ADTs (Algebraic Data Types), and does it really well.

The wiki entry on effect systems[0] tells me that a focus of an effect system is something different from a focus of monads. "The term algebraic effect follows from the type system", where an effect system is effectively a type and effect system. It links to Monadic encapsulation of effects[1] and mentions the runST monad when it mentions support in Haskell, as that one seem to "simulate a type and effect system".

Do have any such a link on the runTS monad?

[0]: https://en.wikipedia.org/wiki/Effect_system

[1]: https://www.cambridge.org/core/journals/journal-of-functiona...

by whilenot-dev

5/30/2026 at 5:48:58 PM

One thing this page makes clear is that do-syntax could mean all kinds of things, which seems like a disadvantage for readability. Assuming you know the specialized syntax, elvis operators looking different from async code or a nested for loop seems like an advantage? The performance implications are entirely different.

by skybrian

5/30/2026 at 12:43:20 PM

Thanks, I definitely feel like I understand monads and their benefits, and even "effects", but I'm not sure what's "algebraic" in "algebraic effects".

by tome

5/30/2026 at 6:00:12 PM

The problem with monads is their composition. Ie. questions like how is the do notation supposed to work if I want to return:

* an Option<Async> or Async<Option>?

* both an Option and an Async (a product, or tuple type)?

* either an Option or an Async (a sum, or tagged union type)?

Monad transformers can be written for wrapping monads into other monads (as a simple example, an Option is equivalent a List with exactly zero or one elements), but they're something of an ad hoc solution and do not generalize well.

This is fundamentally an issue similar to the "function color" problem, or the fact that exceptions in most languages are a limited, ad hoc effect system and do not compose well. Java gave checked exceptions a bad name largely because of their lack of compositionality, but it's more that the particular implementation is poor.

To be fair, that was in 1994 and nobody had worked out these things yet even in the academia. Algebraic effects are the attempt to do just that.

by Sharlin

5/30/2026 at 7:52:48 PM

Right, I understand the history (although I'm not sure I'd say that exception don't compose well) and I understand that "algebraic effects" are an attempt at something better. But I don't understand whether they're something that can be precisely defined or just informal terminology for "a better sort thing for dealing with effects".

by tome

5/30/2026 at 8:31:04 PM

You can precisely define any particular model, but not all work in the area shares the same model. I think you know about the capability-passing model, which is quite different to the algebraic effects (e.g. row types) models.

The general ideas are:

* effects are handled by handlers (called capabilities in the capability-passing model)

* function signatures describe the effects that are used

* effectful code is written in direct style, not monadic style

by noelwelsh

5/31/2026 at 9:58:55 AM

Thanks! This begins to make more sense to me

> effects are handled by handlers

OK, and in the general case a handler allows its body to "perform" an action, and when the action is performed it has the ability to "respond" to it in (in some cases) a very flexible way, running it never, or multiple times, or in a modified environment, or possibly even passing it out of the scope of the handler entirely.

> function signatures describe the effects that are used

Would you say this is not possible in an untyped language then?

> effectful code is written in direct style, not monadic style

I don't understand the distinction here

by tome

5/31/2026 at 8:02:55 PM

> OK, and in the general case a handler allows its body to "perform" an action...

Yes, although not all systems allow this, as implementing full continuations is involved and can hurt performance.

> Would you say this is not possible in an untyped language then?

You can definitely implement the ideas of algebraic effects in an untyped language, but you lose one of the benefits.

> I don't understand the distinction here

Monadic code is code where the order of evaluation is specified by bind / flatMap. Direct-style just uses the language's built-in control flow. See https://noelwelsh.com/posts/direct-style/ for more

by noelwelsh

5/30/2026 at 12:32:26 PM

I'm no expert, but perhaps https://arxiv.org/abs/2302.01415 (especially section 2.2, "Non-Algebraic Effects") will help.

by matt_kantor

5/30/2026 at 12:45:08 PM

Thanks. It says

> Algebraic effects & handlers use a free monad and an interpreter to separate the syntax of effects from their semantics

I'm pretty sure not everybody who works with algebraic effects would say they have to be based on a free monad, so I'm skeptical how definitive this definition is.

by tome

5/30/2026 at 8:41:33 AM

So this looks like dynamically scoped callbacks. Instead of passing callbacks along as parameters they are declared as “handlers”, and any function down the call stack can invoke them. Is this a correct understanding?

by bazoom42

5/30/2026 at 9:28:35 AM

That's what I was thinking. You could get almost all of this pretty directly in Javascript by putting a callback function in an AsyncLocalStorage instance or, in other languages, in a thread local variable.

by AgentME

5/30/2026 at 8:45:24 PM

I was thinking this through more and realized that a callback directly in a thread local variable isn't quite enough: You need to keep a stack in the thread local variable. Every try-handle block pushes a callback in and pops it when exited. (Javascript's AsyncLocalStorage is naturally nestable so you don't need to manage a stack yourself with it.)

by AgentME

5/30/2026 at 10:23:58 AM

You need more than that for the example with setTimeout(). It requires to be able to freeze the stack and then go back later.

You need stackful coroutine (like goroutine) for that.

by _old_dude_

5/30/2026 at 11:15:55 AM

That's very interesting, thanks! It gave me a brainwave and I wondered I could implement that in Bluefin. I'm pretty sure Bluefin's Request[1] is a second class stackful coroutine, and sure enough it turns out to be possible, so I'm pleased about that.

    -- ghci> example
    -- Hello
    -- World
    -- Timed out
    example = runEff $ \io -> awaitYield (receiver io) sender
    
    receiver ::
      (e1 <: es, e2 <: es) =>
      IOE e1 ->
      Await String e2 ->
      Eff es ()
    receiver io a = do
      r1 <- await a
      effIO io (putStrLn r1)
    
      r2 <- await a
      effIO io (putStrLn r2)
    
      mr3 <- timeout io 0 (await a)
      effIO io $ case mr3 of
        Nothing -> putStrLn "Timed out"
        Just r3 -> putStrLn r3
    
    sender ::
      e1 <: es =>
      Yield String e1 ->
      Eff es ()
    sender y = do
      yield y "Hello"
      yield y "World"
      yield y "More"
    
    timeout ::
      e1 <: es =>
      IOE e1 ->
      Int ->
      Eff es r ->
      Eff es (Maybe r)
    timeout io t m = withEffToIO
      (\effToIO -> System.Timeout.timeout t (effToIO (\_ -> useImpl m)))
      io

by tome

5/30/2026 at 12:19:43 PM

Or in Lua you'd wrap the initial call in a coroutine, possibly with coronest[a] or something similar to make handling the effects at the right layer easier.

And so then the outer code is a loop around coroutine.resume, and the inner code uses coroutine.yield to perform an effect.

[a]: https://github.com/saucisson/lua-coronest

by mjmas

5/30/2026 at 9:02:18 AM

Yes. dynamically scoped, and statically typed.

by savq

5/30/2026 at 3:30:34 PM

How can it be statically typed in JavaScript?

by bazoom42

5/31/2026 at 7:02:36 AM

An effect system is an extension of a type system where a function type encodes inputs, outputs and effects.

The article mentions that they handwaved all the typing stuff (when you do that, effect handlers are more like delimited continuations). But the types are important. If a function doesn't tell you it's effects, how do you know you have to handle them? You'd have to read the whole call graph. That's why exceptions suck.

by savq

5/30/2026 at 9:08:49 AM

Standard feature of Common Lisp condition system (over 30 years old).

https://lisp-docs.github.io/cl-language-reference/chap-9/j-b...

by u1hcw9nx

5/30/2026 at 9:47:47 AM

The CL condition system always gets brought up when people unfamiliar with effects see effects for the first time (example: https://news.ycombinator.com/item?id=38813484, another example: https://lobste.rs/s/12m2f0/algebraic_effects_another_mistake).

But while the condition system can do many things you can also do with effects, they cannot do everything.

Here's another discussion on this: https://news.ycombinator.com/item?id=44078743

by brabel

5/30/2026 at 10:10:23 AM

CL conditions do what you actually need if you program. CL gives you deterministic state, safe resource management etc.

Nondeterminism is not a feature you want. Algebraic effects treat the execution stack (continuation) as data, you have total freedom over what you do with it. This flexibility is exactly where you get nondeterminism. This is how logic solvers or probabilistic algorithms work, but you don't want it as a programming language feature in general purpose programming language.

by u1hcw9nx

5/30/2026 at 10:40:25 AM

It’s pretty annoying to comment “this thing was already present in CL 30 years ago”, then to have someone correct you (pointing out that this is a common misconception, and algebraic effects are not equivalent to conditions at all), and then to respond not to admit you’re wrong but instead to say “algebraic effects are bad anyway because you shouldn’t want one of the extra features they give you (nondeterminism)”.

By the way, nondeterminism is not the only difference between the two.

by oisdk

5/30/2026 at 12:41:44 PM

Standard usable feature. Features must work with everything else in the language.

I like programming language theory as much as anyone else, but there is a reason some language features exist only so that people can blog and think about them.

by u1hcw9nx

5/30/2026 at 11:22:40 AM

> Nondeterminism is not a feature you want.

I can get behind the sentiment, but you absolutely need nondeterminism. You can separate the d from the non-d, but only Haskellish languages even attempt it. It's a coarse separation to make (IO vs non-IO), which is where effect systems come in - I guess you can categorise code into more fine-grain buckets. The 'algebraic' part is currently beyond my knowledge.

by mrkeen

5/30/2026 at 9:51:37 AM

One of the things I find most exciting about effect systems is that they solve some of the issues with typeclasses and traits. The issue with these is you can provide exactly one implementation for a type, and this implementation then applies to your entire program. (The underlying theoretical issue, I guess, is coherence.) This means that code cannot request injection of behaviour _except_ through the type of its data.

In practice, what that means is you get a strong temptation to hang behaviour on data even when it doesn't fit perfectly. Because of the natural desire to reduce the number of entity definitions, you end up defining a typeclass on a data type that doesn't fit exactly just to get the behaviour to the right place without having to introduce a new policy or something.

Effects change this by essentially letting you provide multiple names implementations for the same data type, and you don't need to pass around a policy type because the polymorphism lets you tie handlers to scope.

So, if the fancy type-safe library-based control flow doesn't really do much for you, I think that their potential for code design is a good reason to still be excited!

by movpasd

5/30/2026 at 7:35:56 AM

For some funsie here's my fully working delimited continuation in C with effect handler example: https://godbolt.org/z/3ehehvo6E

No ASM involved so technically portable (although it depends on built-in).

Flix equivalent (copy paste to https://play.flix.dev/):

    eff Pick {
        def pick(): Int32
    }

    def body(): (Int32, Int32, Int32) \ Pick = {
        let a = Pick.pick();
        let b = Pick.pick();
        let c = Pick.pick();
        (a, b, c)
    }

    def handlePick(f: Unit -> a \ ef): List[a] \ ef - Pick =
        run {
            f() :: Nil
        } with handler Pick {
            def pick(_, resume) =
                resume(1) ::: resume(2) ::: resume(3)
        }

    def main(): Unit \ IO =
        println(handlePick(body))

by Trung0246

5/30/2026 at 7:07:40 AM

Do effect systems actually avoid colored functions? Don’t most typed effect systems require the used effects in the signature?

by HeyImAlex

5/30/2026 at 7:40:57 AM

Yes, this article is doing a bad job at explaining why you would want effects, and one of the main advantages is exactly that it becomes part of the type system, essentially coloring every single function with a set of effects it needs to be called. As the article used JavaScript it shows what untyped effects would look like, which in my opinion is awful. If you want to use algebraic effects today, I highly recommend Unison. If you’re on the JVM, Flix is doing major advances with effects!

https://www.unison-lang.org/

https://flix.dev/

by brabel

5/30/2026 at 7:27:15 AM

When you need to use an effect, you need it in the type. If you directly call a function using some other effect, it propagates into your function. So far, so colourful.

But you can have generic effects. Your arguments and return type can specify "any effect", indicating your function can use a type with any effect safely, or can be used in any effect context safely.

Passing an async value to a function doesn't mean that function must now also be an async function. It can be a "for all effects, do the thing" function. The code duplication problem is gone.

by codebje

5/30/2026 at 7:32:00 AM

No, they are function colouring. That's the point.

Someone writes a post lamenting red and blue functions, and everyone eats it up.

Substitute colour for something meaningful and the idea becomes idiotic.

"Top level function declares that it is non-blocking, but when I try to call a small blocking function from it, I have to change the declaration to blocking???"

Yes, yes you do.

Total functions can't call non-total functions.

Deterministic functions can't call nondeterministic functions.

Non-IO functions can't call IO functions.

by mrkeen

5/30/2026 at 9:21:08 AM

‘Non-IO functions can't call IO functions.’

How do you handle logging then? If f() calls g(), how can I add logging to g() without having to change or recompile f() (and everything in the call stack above it)? ‘You can’t’ is not an acceptable answer.

by captaincrowbar

5/30/2026 at 10:25:02 AM

Not sure why people are saying "you can't" when it seems to me the whole point of algebraic effects that you can. You can define g so that it has no ability to do "general IO", all it can do is yield log messages. Then f can call g in a way that turns the log messages into writes to stdout. For example, here's how you would do it in Bluefin:

    type Log = Yield String
    
    -- workWithLogging cannot do arbitrary IO!
    -- All it can do is yield log messages, which
    -- must be processed elsewhere.
    workWithLogging  ::
      (e1 :> es) =>
      Log e1 ->
      Int ->
      Int ->
      Eff es Int
    workWithLogging l x y = do
      yield l ("x was " <> show x)
      yield l ("y was " <> show y)
      let result = x + y
      yield l ("result was " <> show result)
      pure result
    
    -- ghci> example
    -- x was 5
    -- y was 7
    -- result was 12
    -- 12
    example :: IO Int
    example = runEff $ \io -> do
      -- forEach determines how each log message
      -- should be handled.
      forEach
        (\l-> workWithLogging l 5 7)
        (\logMsg -> effIO io (putStrLn logMsg))

by tome

5/30/2026 at 11:04:54 AM

"You can't" is simpler, because the inevitable reply is "but how do I do actual logging inside g"

by mrkeen

5/30/2026 at 11:17:23 AM

"Actual logging" as in direct access to IO?

by tome

5/30/2026 at 11:36:54 AM

Yes

by mrkeen

5/30/2026 at 9:51:07 AM

Don't declare it as non-logging.

by mrkeen

5/30/2026 at 10:04:58 AM

You can’t is an acceptable answer. The entitle point of such a feature is to prevent people from doing that.

by CuriousSkeptic

5/30/2026 at 1:27:14 PM

Good article! I've been struggling to grasp what these new "effects" were.

When I had been in university twenty years ago, I had constructed a language with "effects" as I had understood them then: side-effects annotations. I could not in my head recognise how the new effects were like those I had read about in the literature back then.

More recently, I've been working on a compiler back-end/runtime in my (too much) free time with support for resumable exceptions. I didn't know it before, but after reading this article, it appears that the runtime actually does have support for "effects", without me really trying.

by Findecanor

5/30/2026 at 10:37:24 AM

As the article notes, effects (at least as described in the article) are already possible in JavaScript using generators, as long as every calling function is a generator, which I assume the author finds problematic because generators are cumbersome.

I was wondering how well TypeScript can type generator-based effects. My hypothesis is that TypeScript can let you compose functions with effects, but it is not possible for it to narrow down that the result from an effect corresponds to the effect (i.e. the result of an effect will be any possible effect result used by the function).

My incomplete, untested attempt in TypeScript[0] tries to implement `enumerateFiles` and `withMyLoggingLibrary`. The type errors demonstrate TypeScript's limitation that it can't associate an effect call with its result.

[0]: https://www.typescriptlang.org/play/?#code/C4TwDgpgBAMg9gcwK...

by sheept

5/30/2026 at 10:01:41 AM

Pardon my ignorance, but isn’t this more or less a fancy goto?

by kenanfyi

5/30/2026 at 12:46:19 PM

Sure, but only in the sense that all structured programming (functions, if/else, loops, etc) is "fancy goto".

by matt_kantor

5/30/2026 at 1:47:44 PM

I believe the answer is "yes, but..."

Yes, but every every control flow statement in programming is more or less a fancy goto.

If, do, while, for, try/catch, ...

by BoiledCabbage

5/30/2026 at 7:33:57 AM

Everything he lists is solved by effect-ts [1] bar, obviously, the language support (effect has its own fiber-based runtime like ZIO's scala).

I've been using it for 5+ years and my 4 men team can scale to supporting 6 different products (each running millions $ in business, sometimes daily), as we reuse the same patterns and architecture. This would not be possible without Effect, even though I'm lucky to have terrific engineers as colleagues, we just wouldn't be able to without the endless goodies from Effect.

The amount of features is basically endless, as effects and runtimes weren't enough, from SQL to AI, from effectful schemas (encoders/decoders), first-class OTEL support, CLI, debuggers, editor extensions, and many others. There's still countless modules I have yet to see or use.

Runtimes are available for each platform, including cloudflare workers.

There's absolutely nothing in TypeScript land to have such a wide scope.

v4 will also bring durable workflows (I'm already using v4 beta and that feature in prod) and many other goodies. That's quite important for us needing to have procedures that need to survive redeploys, crashes, etc.

I would never go back to writing standard TypeScript.

There is a learning curve, but you can adopt it incrementally. Nobody adopting it has ever gone back.

That being said, it would be great if there was a proper effect-based language (I've seen few projects like Effekt, but there's way too many things missing) as TypeScript is verbose, and effect adds its own verbosity.

[1] https://effect.website/

by epolanski

5/30/2026 at 7:42:14 AM

Effect is pretty nice, I'm not sure how worth it it is for the frontend, but I've heard good things on the backend, but sadly I don't use TypeScript for backend work, mainly Rust, and would love to see something like that there. I'm not sure how much Rust's type system would make it possible though however.

I know parts of Effect like its schema are incrementally adoptable but if you use it substantially with many of its features, isn't it viral in a sense? In that you need to do things the Effect way and wrap libraries into Effect functions?

by satvikpendem

5/30/2026 at 8:12:18 AM

It does tend to naturally bubble upwards as you point out, but you can decide where to stop.

E.g. you could describe a complex effect that has retry, scheduling, etc and run it only once with `Effect.runFork(yourEffect)` in a random place of your existing code.

That's in general how teams adopt it, in general there's a champion in the team that sells using one feature, and as people get accustomed and the champion does a good work mentoring it slowly takes over whole projects.

by epolanski

5/30/2026 at 10:45:23 AM

When I started a typescript server project, I spent quite some time - setting up dependency injection framework to pass context (tsyringe) - finding a good schema validation library (zod) - use neverthrow for typed error. - wrote a few withSpan, recordDuration OTEL helper functions.

Then I heard about Effect-ts, checked the doc, and realize that Effect-ts already has all these things, in a single package.

IMO Effect-ts is currently the most practical effect system right now. While it does not support resuming like in other algebraic effect languages, it is powerful enough to express common patters, but not too powerful so that the code becomes hard to understand.

I hope effect-ts gets more traction. The biggest obstacle to sell it right now is that the API doc is not great. I had to trace the source code several times just to see how a type is defined. I hope Effect-ts team is aware that more people will use it if it has a proper documentation.

by bobogei81123

5/30/2026 at 8:03:01 AM

The learning curve is quite steep but once you get it you become effect pilled. You can completely separate the WHAT the application does from HOW it will do it.

by mthewood

5/30/2026 at 9:02:57 AM

Effect is unreasonably effective. Pun etc.

The problem with these concepts is a) they are completely opaque to the common chud programmer and b) they are just not available to people in languages that anyone actually uses. There are a bunch of effect libraries in Haskell, even special efforts to make them work better in GHC, but it's nearly all wasted effort because it's just academic circlejerk.

Effect brings these capabilities to the masses by implementing them in the most popular programming language on the planet. Obviously, there is quite a learning curve -- it is essentially a programming language unto its own inside another programming language -- but it's doable. I've onboarded juniors with close to 0 FP experience into an Effect codebase. The guardrails help a lot. The language server which helps with best practices, the type errors themselves help quite a lot.

Arguably the best way to do Effect would be a separate programming language, but that would just give us the problems Haskell has: nobody would use since there would be no ecosystem, and there will be no ecosystem since nobody would use it.

by IceDane

5/30/2026 at 3:27:31 PM

While the effect system seems way more general, it’s a bit sad the example only talk about logging or IO, both of which can be simply done with dependency injection

by KolmogorovComp

5/30/2026 at 5:12:31 PM

It's my belief that all effects can be done with dependency injection in some form, at least, I'm not familiar with ones that can't. Even arbitrary delimited continuations can be implemented by injecting a reference to the continuation prompt on the stack.

by tome

5/30/2026 at 8:25:22 AM

I’ve used effects in scala3 with cats-effects and haven’t been impressed. All in all, the way it was used was just to reimplement a very similar interface to exceptions.

by KolmogorovComp

5/30/2026 at 9:08:48 AM

Cats Effect is monadic effects. What is discussed here is sometimes called "direct-style" effects, and is an alternative representation.

I think you've missed the point regarding effect systems. Concurrency and resource handling, implemented in a way that is composable and reasonably easy to reason about, are two of the big ticket features.

by noelwelsh

5/30/2026 at 9:21:07 AM

I swear I'm not trying to be inflamatory, but this is the _worst_ programming language feature I could ever imagine. I'm not trying to be hyperbolic, if I try to reason about it there is nothing I can come up with that I would dislike more in the realm of recent features that have been pitched in the PL community

I was already in the camp that try/catch is "considered harmful", I dislike the concept of having a second, hidden, control flow that might get sprung up upon function callers, because it has side effects buried in the implementation of a callee that are not defined in the parameters or the returns, and I am not 100% sold on the benefits of "Things in the middle don’t need to concern themselves with error handling.", which I guess informs this opinion.

Now since I hate that, I really, really would hate that on top of this, another programmer could write a hidden control flow upstairs that could, potentially, not just crash my code, but also do a lot of other things, such as coming up with default values for unexpected NULLs or whatever, which could THEN take something that would have crashed immediately, and turn it into something that crashes later down the line, away from the problem, with a varialble set to an inexplicable value that I have never put there myself

What a nightmare to debug! I mean, come on

by sebstefan

5/30/2026 at 10:31:14 AM

> I dislike the concept of having a second, hidden, control flow that might get sprung up upon function callers, because it has side effects buried in the implementation of a callee that are not defined in the parameters or the returns

You might like my capability-based effect system for Haskell, Bluefin[1], then. If a Bluefin effectful function throws you can see it in the type system. If you want to have the capability to throw, you need to pass in an argument of type Throw. For example here "workWithThrow" can only throw an exception because it is passed the Throw capability.

    workWithThrow  ::
      (e1 :> es) =>
      Throw String e1 ->
      Int ->
      Int ->
      Eff es Int
    workWithThrow t x y = do
      let result = x + y
      when (result > 10) $ do
        throw t "Too big"
      pure result
    
    -- ghci> example
    -- Left "Too big"
    example :: Either String Int
    example = runPureEff $ try $ \t -> do
      workWithThrow t 5 7

[1] https://hackage.haskell.org/package/bluefin

by tome

5/30/2026 at 10:34:42 AM

I agree but I also think it's important to point out that Algebraic Effects typically refers both to a runtime feature (the try/handle delineated continuation stuff) and also a type system feature.

The latter is very important because in your example it would not really be hidden. If your function does not have the "exn" effect, it cannot call functions that throw exceptions, full stop. Same with any other effect including IO if you want.

Basically function coloring taken to the extreme. In a statically typed language with statically typed effects you actually cannot get surprised, which was your major complaint.

Type systems that support algebraic effects also typically support row polymorphic effects (fancy generics) so you can make a function generic over "color", avoiding the "function coloring" problem.

Now, having said that, why did I say I agree with you? Well because algebraic effects are a lousy user-facing feature. You almost never want to implement your own handlers, at best you'll plug in a custom handler for the IO effect and that's about it. (And for exceptions of course, but that's just exceptions with extra steps)

Where they shine is for the language implementer. They provide a framework on which exceptions, generators, async/await and even prolog-like backtracking can be implemented, while (very importantly) defining how they compose. That's really the bit that makes them so interesting from a research point of view and why they might make it into the mainstream languages, even if the language doesn't actually ever expose them for you to use.

by sirwhinesalot

5/30/2026 at 9:58:10 AM

Because you must statically declare dependency on an effect, it's opt-in.

To be clear, you still pay indirection cost: when you do opt in you have to hope the upstairs implementation is compliant to the contract. But that does also apply to interfaces/typeclasses.

by movpasd

5/30/2026 at 10:20:51 AM

[dead]

by gobdovan