Effects

Algebraic effects for IO, errors, and more.

The Problem

Here is the oldest question in programming: how do you test code that talks to databases, reads files, or makes network calls? Most languages answer this with mocking frameworks, dependency injection containers, and layers of interfaces. It works, but it is a lot of ceremony for a simple idea.

Loon takes a different path. With algebraic effects, your functions declare what they need ("I want to read from a store") without specifying how it happens. The caller decides the implementation. This might sound abstract right now, but it is surprisingly intuitive once you see it in action.

Declaring Effects

An effect is a set of operations with no implementation. Think of it like an interface or trait, but lighter weight.

LOON
[effect Store
  [fn get [key]]
  [fn set [key value]]]

This says "there exists a thing called Store with get and set operations." It says nothing about where data lives, whether it is in memory, on disk, or across the network. That decision belongs to whoever handles the effect.

Performing Effects

Once you have declared an effect, you call its operations with dot syntax, just like calling a method. It looks completely natural.

LOON
[fn save-user [name]
  [Store.set "user" name]
  [Store.get "user"]]

Notice that save-user has no idea how Store works. It just uses it. This is the key insight: the function describes its intent, not its mechanism.

Handling Effects

A handle block intercepts effect operations and gives them concrete behavior. This is where you wire an effect to a real implementation.

LOON
[handle [save-user "Alice"]
  [Store.get key] [get db key]
  [Store.set key val] [insert db key val]]

Each clause pattern-matches on an effect operation and provides the actual logic. When save-user calls Store.set, execution jumps to the matching handler clause, runs it, and then returns the result back to the function.

Resume

Handlers receive an implicit resume function that continues execution right where the effect was performed. This is what makes effects fundamentally different from exceptions.

LOON
[handle [do [Store.set "x" 1] [Store.get "x"]]
  [Store.get key] [resume [get state key]]
  [Store.set key val] [resume None]]

When an exception fires, the stack unwinds and you lose your place. When an effect fires and you call resume, you pick up exactly where you left off. The effectful function does not even know it was interrupted.

Calling resume is what makes effects different from exceptions. Execution continues where it left off, as if the effect operation simply returned a value.

Built-in Effects

Loon ships with a few built-in effects for common operations so you do not have to define them yourself.

IO

The IO effect covers the basics: printing, reading files, and network access. Anything that touches the outside world goes through IO.

LOON
[IO.println "hello"]
[let contents [IO.read-file "data.txt"]]

Fail

The Fail effect represents recoverable errors. Instead of throwing exceptions that might go uncaught, you perform a Fail effect that a handler somewhere up the call stack will deal with.

LOON
[fn divide [a b]
  [if [= b 0]
    [Fail.raise "division by zero"]
    [/ a b]]]

The ? Operator

Unwrapping Results manually gets tedious fast. The ? operator is syntactic sugar that unwraps a Result on success, and automatically performs Fail on error. It is Loon's version of the "try" pattern you may know from Rust or Swift.

LOON
[fn load-config [path]
  [let text [IO.read-file path]?]
  [parse-json text]?]

Without ?, you would need to match on each Result and handle the error case explicitly. With it, the happy path reads top to bottom, and errors propagate automatically.

Effect Annotations

By convention, functions that perform effects end with ! in their name. The compiler does not enforce this, but your teammates will thank you for it.

LOON
[fn save-user! [name]
  [Store.set "user" name]]

When you see a ! at the end of a function name, you immediately know it does something beyond pure computation. It might write to a database, print to the console, or launch a rocket. The bang is a social contract: "this function has side effects, handle with care."

Testing with Effects

This is where algebraic effects really shine. Testing effectful code is trivial because you just swap in a different handler. No mocking libraries. No dependency injection frameworks. No test doubles that implement twelve interface methods when you only care about two.

LOON
[test "save-user stores correctly" []
  [let mut log #[]]
  [handle [save-user "Bob"]
    [Store.set k v] [do [push! log v] [resume None]]
    [Store.get k] [resume "Bob"]]
  [assert-eq log #["Bob"]]]

In production, your handler talks to a real database. In tests, it records calls into a vector. The function under test is identical in both cases. It has no idea who is handling its effects, and that is exactly the point.