osa1 github about atom

Why I'm excited about effect systems

June 28, 2025 - Tagged as: en, plt.

Imagine a programming language where you can have full control over whether and how functions, modules, or libraries interact with shared resources like the scheduler for threading, the file system and other OS-level resources like sockets and other file descriptors, timers for things like delaying the current thread for timed updates or scheduling timed callbacks, and so on.

In this language, a function (or module, library, …) needs to declare its interactions with the shared resources in its type.

When a function accesses e.g. the file system, the caller has full control over how it accesses the file system. All file system access functions can be specified (or overridden if they have a default) by the caller.

Furthermore, assume that this language can also suspend functions and resume them later, similar to async functions in many languages today, which are paused and resumed later when the value of e.g. a Future becomes available.

This language lends itself to a more composable system compared to anything that we have today. This system is composable, flexible, and testable by default.

If you think about it, it’s really strange that today we find it acceptable that I can import a library, and the library can spawn threads, use the file system, block the current thread with things like sleep or with blocking IO operations, and I have no control over it.

Most of the time, this kind of thing will be at least documented, but if I use a library that fundamentally needs these things, unless the library accounts for my use case, I may not be able to use it in my application.

For example, maybe it spawns threads but I want it to use my own thread pool where in addition to limiting number of threads, I attach priorities to threads and schedule based on priorities.

Or, maybe I have a library that builds/compiles things by reading files, processing them, and generating files. If I have control over the file system API that the library uses, it takes no effort (e.g. no planning ahead of time) to test this library using an in-memory file system, in parallel, without worrying about races and IO bottlenecks. I don’t have to consider testing scenarios in the library and structure my code accordingly.

Or, maybe I have code that polls some resources, and maybe posts periodic updates. It creates a thread that does the periodic work, and sleeps. With control over threads, schedulers, and timers, I can fast-forward in time (to the next event) in my tests without actually waiting for sleeps and any other timed events, to test my code quickly.

These are some of the things I get to do with an effect system.

What’s in an effect system?

At a high-level, an effect system has two components: (1) a type system, and (2) runtime features.

These two components are somewhat orthogonal: you can have one without the other, depending on what you want to make possible.

In the systems available today, (1) typically involves adding a type component to function types, for the effects a function can invoke.1

For example, in Koka, if you define stdin/stdout operations in an effect named console, and have a function that uses the console effects, the function’s type signature looks like this:

fun sayHi() -> console ()
  print("hi")

This type says sayHi returns unit (()) and uses the console effect.

(2) typically involves capturing the continuation of the effect invocation and passing it to a “handler”. Depending on the system, the handler can then do things (e.g. memory operations, invoking other effects) and “jump” to (or “tail call”) the continuation with the value returned by the invoked effect.

With the console effect above, a handler may just record the printed string in a data structure, which can then be used for testing. Another handler may actually write to stdout, which would then be used when you run the application.

Depending on the exact (1) and (2) features, you get to do different things. The current effect systems in various languages support different (1) and (2) features, and there are some systems that omit one of (1) or (2) entirely.

For the purposes of this blog post, we won’t consider the full spectrum of features you can have, and what those features allow.

Example: a simple grep implementation in Koka

There isn’t a language today that gives us everything we need for the use cases I describe at the beginning.

However among the languages that we have, Koka comes close, so we’ll use Koka for a simple example.

Imagine a simple “grep” command that takes a string and a list of file paths as arguments, and finds occurrences of the string in the file contents and reports them.

In Koka, the standard library definitions for these “effects” could look like this:

effect fs
  ctl read-file(path: path): string

effect console
  ctl println(s: string): ()

Using these effects, the code that reads the files and searches for the string is not different from how it would look like in any other “functional”2 language:

fun search(pattern: string, files: list<string>): <fs, console>()
  val pattern-size = pattern.count()
  files.foreach fn(file)
    val contents = read-file(file.path)
    val parts = contents.split(pattern)
    report-matches(file, pattern-size, parts)

fun report-matches(file: string, pattern-size: int, parts: list<string>): <console>()
  if parts.length == 0 then
    return ()

  println(file)

  var line := 0
  var column := 0
  parts.init.foreach fn(part)
    part.vector.foreach fn(char)
      if char == '\n' then
        line := line + 1
        column := 0
      else
        column := column + 1

    println((line + 1).show ++ ":" ++ (column + 1).show)

When calling search, I have to provide handlers for fs and console effects.

In the executable that I generate for users, I can use handlers that do actual file system operations and print to stdout:

val fs-io = handler
  ctl read-file(path: path)
    resume(read-text-file(path))

val console-terminal = handler
  ctl println(s: string)
    write-to-stdout(s)
    resume(())

In the tests, I can use a read-file handler that reads from an in-memory map, and add printed lines to a list, to compare with the expected test outputs:

struct test-case
  files: list<test-file>
  pattern: string
  expected-output: list<string>

struct test-file
  path: path
  contents: string

val test-cases: list<test-case> = [
  Test-case(
    files = [Test-file("file1".path, "test\ntest"), Test-file("file2".path, "a\n test\nb")],
    pattern = "test",
    expected-output = ["file1", "1:1", "2:1", "file2", "2:2"]
  ),
]

fun test(): <exn>()
  var printed-lines := Nil

  test-cases.foreach fn (test)
    with handler
      ctl read-file(path_: path)
        match test.files.find(fn (file) file.path.string == path_.string)
          Just(file) -> resume(file.contents)
          Nothing -> throw("file not found", ExnAssert)

    with handler
      ctl println(s: string)
        printed-lines := Cons(s, printed-lines)
        resume(())

    search(test.pattern, test.files.map(fn (file) file.path.string))

    if printed-lines.reverse != test.expected-output then
      throw("unexpected test output", ExnAssert)

You can see the full example here.

I can already do this in language X using library/framework Y?

The point with effect systems is that, you don’t get a composable and testable system when you design for it, you get it by default.

If you implement a library that uses the file system, I can run it with an in-memory file system, or intercept file accesses to prevent certain things, or log certain things, and so on, regardless of whether you designed for it or not.

The Koka code above does not demonstrate this fully, and there’s no system available today that can. I’m just using whatever is available today.

In an ideal system, you would have to go out of your way to have access to the filesystem without using an effect, rather than the other way around.

When comparing languages we never talk about what’s possible: almost everything is possible in almost every general purpose programming language.

What we’re talking about is things like: the idiomatic and performant way of doing things.

The language where what I talk about is idiomatic and performant does not exist today.

How do we know that this ideal system is possible?

We mentioned that the two components of an effect system are somewhat orthogonal. In the design that I have in mind (more on this below), without the type system part of it you still get 90% of the benefits. So let’s focus on the runtime parts.

What you need for a flexible effect system is, conceptually, a way of suspending the stack when calling an effect, passing the suspended stack (you may want to call it a “continuation”) to the handler for the effect invoked.

This kind of thing is already possible in many of the high-level languages today. If your language supports lightweight threads (green threads, fibers, etc.), coroutines, generators, or similar features where the code is suspended when it does something like await or yield, and then resumed later, you already have the runtime features for a flexible effect system.

For me, it’s about composable and testable libraries

I deliberately didn’t mention in this blog post so far that effect systems generalize features like async/await, iterators/generators, exceptions, and many other features.

The reason is because, as a user, I don’t care whether these features are implemented using an effect system under the hood, or in some other ways. For example, Dart has all of these features, but it doesn’t use an effect system to implement them. As a user, it doesn’t matter to me as long as I have the features.

Instead, what I’m more interested in as a user is: how it influences or affects library design, and what it allows me to do at a high level, in large code bases.

However it would be a shame to not mention that, yes, effect systems generalize all these features, and more. The paper “Structured Asynchrony with Algebraic Effects” shows how these features can be implemented in Koka.

To be continued

Some of the recent discussions online about effect systems left me somewhat dissatisfied, because most posts seem to focus on small-scale benefits of effect systems, and I wanted to share my incomplete (but hopefully not incoherent!) perspective on effect systems.

In the future posts I’m hoping to cover some of the open problems when designing such a system.


Thanks to Tim Whiting for reviewing a draft of this blog post.


  1. This is a somewhat rough estimate on what these effect types in function types indicate. In practice it’s more complicated than “effects the function invokes”: if you read it as that you fail to explain some of the type errors, or why some code of the code type checks. More on this (hopefully) in a future post.↩︎

  2. “Functional” in quotes because I don’t think that word means much these days. Maybe more on this later.↩︎