Skip to content
HN On Hacker News ↗

Algebraic Effects for the Rest of Us — overreacted

▲ 107 points 89 comments by satvikpendem 2mo ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully human-written

0 %

AI likelihood · overall

Human
100% human-written 0% AI-generated
SEGMENTS · HUMAN 6 of 6
SEGMENTS · AI 0 of 6
WORD COUNT 1,831
PEAK AI % 0% · §3
Analyzed
May 30
backend: pangram/v3.3
Segments scanned
6 windows
avg 305 words each
Distribution
100 / 0%
human / AI fraction
Verdict
Human
Pangram v3.3

Article text · 1,831 words · 6 segments analyzed

Human AI-generated
§1 Human · 0%

Have you heard about algebraic effects? My first attempts to figure out what they are or why I should care about them were unsuccessful. I found a few pdfs but they only confused me more. (There’s something about academic pdfs that makes me sleepy.) But my colleague Sebastian kept referring to them as a mental model for some things we do inside of React. (Sebastian works on the React team and came up with quite a few ideas, including Hooks and Suspense.) At some point, it became a running joke on the React team, with many of our conversations ending with:

It turned out that algebraic effects are a cool concept and not as scary as I thought from those pdfs. If you’re just using React, you don’t need to know anything about them — but if you’re feeling curious, like I was, read on. (Disclaimer: I’m not a programming language researcher, and might have messed something up in my explanation. I am not an authority on this topic so let me know!) Not Production Ready Yet Algebraic Effects are a research programming language feature. This means that unlike if, functions, or even async / await, you probably can’t really use them in production yet. They are only supported by a few languages that were created specifically to explore that idea. There is progress on productionizing them in OCaml which is… still ongoing. In other words, Can’t Touch This.

Edit: a few people mentioned that LISP languages do offer something similar, so you can use it in production if you write LISP.

So Why Should I Care? Imagine that you’re writing code with goto, and somebody shows you if and for statements. Or maybe you’re deep in the callback hell, and somebody shows you async / await. Pretty cool, huh? If you’re the kind of person who likes to learn about programming ideas several years before they hit the mainstream, it might be a good time to get curious about algebraic effects. Don’t feel like you have to though. It is a bit like thinking about async / await in 1999. Okay, What Are Algebraic Effects? The name might be a bit intimidating but the idea is simple. If you’re familiar with try / catch blocks, you’ll figure out algebraic effects very fast. Let’s recap try / catch first.

§2 Human · 0%

Say you have a function that throws. Maybe there’s a bunch of functions between it and the catch block: function getName(user) { let name = user.name; if (name === null) { throw new Error('A girl has no name'); } return name; } function makeFriends(user1, user2) { user1.friendNames.push(getName(user2)); user2.friendNames.push(getName(user1)); } const arya = { name: null, friendNames: [] }; const gendry = { name: 'Gendry', friendNames: [] }; try { makeFriends(arya, gendry); } catch (err) { console.log("Oops, that didn't work out: ", err); } We throw inside getName, but it “bubbles” up right through makeFriends to the closest catch block. This is an important property of try / catch. Things in the middle don’t need to concern themselves with error handling. Unlike error codes in languages like C, with try / catch, you don’t have to manually pass errors through every intermediate layer in the fear of losing them. They get propagated automatically. What Does This Have to Do With Algebraic Effects? In the above example, once we hit an error, we can’t continue. When we end up in the catch block, there’s no way we can continue executing the original code. We’re done. It’s too late. The best we can do is to recover from a failure and maybe somehow retry what we were doing, but we can’t magically “go back” to where we were, and do something different. But with algebraic effects, we can. This is an example written in a hypothetical JavaScript dialect (let’s call it ES2025 just for kicks) that lets us recover from a missing user.name: function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.push(getName(user2));

§3 Human · 0%

user2.friendNames.push(getName(user1)); } const arya = { name: null, friendNames: [] }; const gendry = { name: 'Gendry', friendNames: [] }; try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { resume with 'Arya Stark'; } } (I apologize to all readers from 2025 who search the web for “ES2025” and find this article. If algebraic effects are a part of JavaScript by then, I’d be happy to update it!) Instead of throw, we use a hypothetical perform keyword. Similarly, instead of try / catch, we use a hypothetical try / handle. The exact syntax doesn’t matter here — I just came up with something to illustrate the idea. So what’s happening? Let’s take a closer look. Instead of throwing an error, we perform an effect. Just like we can throw any value, we can pass any value to perform. In this example, I’m passing a string, but it could be an object, or any other data type: function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } When we throw an error, the engine looks for the closest try / catch error handler up the call stack. Similarly, when we perform an effect, the engine would search for the closest try / handle effect handler up the call stack: try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { resume with 'Arya Stark'; } } This effect lets us decide how to handle the case where a name is missing. The novel part here (compared to exceptions) is the hypothetical resume with: try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { resume with 'Arya Stark'; } } This is 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.

§4 Human · 0%

🤯 function getName(user) { let name = user.name; if (name === null) { // 1. We perform an effect here name = perform 'ask_name'; // 4. ...and end up back here (name is now 'Arya Stark') } return name; } // ... try { makeFriends(arya, gendry); } handle (effect) { // 2. We jump to the handler (like try/catch) if (effect === 'ask_name') { // 3. However, we can resume with a value (unlike try/catch!) resume with 'Arya Stark'; } } This takes a bit of time to get comfortable with, but it’s really not much different conceptually from a “resumable try / catch”. Note, however, that algebraic effects are much more flexible than try / catch, and recoverable errors are just one of many possible use cases. I started with it only because I found it easiest to wrap my mind around it. A Function Has No Color Algebraic effects have interesting implications for asynchronous code. In languages with an async / await, functions usually have a “color”. For example, in JavaScript we can’t just make getName asynchronous without also “infecting” makeFriends and its callers with being async. This can be a real pain if a piece of code sometimes needs to be sync, and sometimes needs to be async. // If we want to make this async... async getName(user) { // ... } // Then this has to be async too... async function makeFriends(user1, user2) { user1.friendNames.push(await getName(user2)); user2.friendNames.push(await getName(user1)); } // And so on... JavaScript generators are similar: if you’re working with generators, things in the middle also have to be aware of generators.

§5 Human · 0%

So how is that relevant? For a moment, let’s forget about async / await and get back to our example: function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.push(getName(user2)); user2.friendNames.push(getName(user1)); } const arya = { name: null, friendNames: [] }; const gendry = { name: 'Gendry', friendNames: [] }; try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { resume with 'Arya Stark'; } } What if our effect handler didn’t know the “fallback name” synchronously? What if we wanted to fetch it from a database? It turns out, we can call resume with asynchronously from our effect handler without making any changes to getName or makeFriends: function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.push(getName(user2)); user2.friendNames.push(getName(user1)); } const arya = { name: null, friendNames: [] }; const gendry = { name: 'Gendry', friendNames: [] }; try { makeFriends(arya, gendry); } handle (effect) { if (effect === 'ask_name') { setTimeout(() => { resume with 'Arya Stark'; }, 1000); } } In this example, we don’t call resume with until a second later. You can think of resume with as a callback which you may only call once. (You can also impress your friends by calling it a “one-shot delimited continuation.”) Now the mechanics of algebraic effects should be a bit clearer. When we throw an error, the JavaScript engine “unwinds the stack”, destroying local variables in the process.

§6 Human · 0%

However, when we perform an effect, our hypothetical engine would create a callback with the rest of our function, and resume with calls it. Again, a reminder: the concrete syntax and specific keywords are made up for this article. They’re not the point, the point is in the mechanics. A Note on Purity It’s worth noting that algebraic effects came out of functional programming research. Some of the problems they solve are unique to pure functional programming. For example, in languages that don’t allow arbitrary side effects (like Haskell), you have to use concepts like Monads to wire effects through your program. If you ever read a Monad tutorial, you know they’re a bit tricky to think about. Algebraic effects help do something similar with less ceremony. This is why so much discussion about algebraic effects is incomprehensible to me. (I don’t know Haskell and friends.) However, I do think that even in an impure language like JavaScript, algebraic effects can be a very powerful instrument to separate the what from the how in the code. They let you write code that focuses on what you’re doing: function enumerateFiles(dir) { const contents = perform OpenDirectory(dir); perform Log('Enumerating files in ', dir); for (let file of contents.files) { perform HandleFile(file); } perform Log('Enumerating subdirectories in ', dir); for (let directory of contents.dir) { // We can use recursion or call other functions with effects enumerateFiles(directory); } perform Log('Done'); } And later wrap it with something that specifies how: let files = []; try { enumerateFiles('C:\\'); } handle (effect) { if (effect instanceof Log) { myLoggingLibrary.log(effect.message); resume; } else if (effect instanceof OpenDirectory) { myFileSystemImpl.openDir(effect.dirName, (contents) => { resume with contents; }); } else if (effect instanceof HandleFile) { files.push(effect.fileName); resume; } } // The `files` array now has all the files Which means that those pieces can even become librarified: