What Are Algebraic Effects?
Understanding effects as capabilities tracked by types
Understanding What Are Algebraic Effects?
Effects are capabilities your program needs to execute—like logging, reading state, failing with errors, or waiting on async operations.
In typelang, effects are NOT performed directly. Instead, you create effect operations that describe what you want to do.
The runtime resolves these operations through handlers, keeping your code pure and testable.
Think of effects as 'resumable exceptions'—you can intercept them, transform them, or provide custom behavior without touching the original code.
Code Example
// Traditional imperative (hidden effects)
function processUser(id: string) {
console.log(`Processing ${id}`); // Hidden effect!
const user = fetchUser(id); // Hidden IO!
if (!user) throw new Error("404"); // Hidden exception!
return user;
}
// typelang (explicit effects)
const processUser = (id: string) =>
seq()
.do(() => Console.op.log(`Processing ${id}`))
.let(() => fetchUser(id))
.let((user) =>
match(option(user), {
None: () => Exception.op.fail({tag: "NotFound", id}),
Some: ({value}) => value,
})
)
.return((result) => result);
// Type signature reveals all effects using record-based capabilities
type ProcessUserCaps = Readonly<{
console: typeof Console.spec;
exception: typeof Exception.spec;
http: typeof Http.spec;
}>;
// Explicit signature: Eff<User, ProcessUserCaps>
Key Points
- Effects are tracked in the type signature as Eff<A, Caps>
- A is the return value, Caps are the required capabilities (use record syntax)
- Record-based capabilities: { console: Console; http: Http } are self-documenting
- Operations like Console.op.log() return Eff, not void
- Handlers interpret effects at runtime—swap handlers for different contexts
- Your code stays pure; side effects happen in handlers