A single-header algebraic effects library for C++23.
Effects let you write effectful code — I/O, failure, logging, async, generators — in a direct style, while keeping the implementation of those effects completely separate from the code that performs them. Handlers are swappable at the call site without changing the coroutine body.
struct Ask : Effect<std::string> {
std::string prompt;
};
auto greet() -> Row<Ask>::Fx<std::string> {
auto name = perform(Ask{.prompt = "Name: "});
co_return "Hello, " + name + "!";
}
struct AskHandler : Handler<Ask> {
void handle(Ask e, auto r) {
r("Alice");
}
}
int main() {
std::cout << greet().run(AskHandler{}) << "\n"; // Hello, Alice!
}The return type encodes every effect the function can perform. Handlers are validated at compile time — missing one is a build error with an IDE diagnostic.
- C++23 (
-std=c++23) - GCC 13+ or Clang 17+
- Single header:
#include "effects.hpp"
#include "effects.hpp"
// 1. Define effects
struct Log : Effect<std::monostate> {
std::string message;
};
// 2. Write effectful code
auto counted_sum(int n) -> Row<Log>::Fx<int> {
int total = 0;
for (int i = 1; i <= n; ++i) {
perform(Log{.message = "adding " + std::to_string(i)});
total += i;
}
co_return total;
}
struct LogHandler : Handler<Log> {
void handle(Log e, auto r) {
std::cout << e.message << "\n";
r({});
}
}
// 3. Run with a handler
int main() {
int result = counted_sum(3).run(LogHandler{}); // prints 3 lines, returns 6
}| Topic | Description |
|---|---|
| Effects & Rows | Defining effects, grouping them into rows, nested rows |
| Handlers | Named handlers, lambda handlers, composite handlers, VALIDATE_HANDLER |
| Propagation | How effects flow through co_await chains automatically |
| Composition | .bind(), local handling, stripping effects mid-chain |
| Validation | Compile-time checks, IDE diagnostics, the deleted-function pattern |
| Performance | Benchmark results, ScopedAllocator, PMR strategies, allocator guide |
| Row Polymorphism | HOF patterns, AnyFx, FxMapper, Rebind, FxOf, FxValue |
| API Reference | Complete listing of every public type, function, concept, and macro |
// type param = the value the handler sends back
struct Fail : Effect<int> {
std::string reason;
};using IO = fx::Row<Ask, Log>;
auto prompt_and_log() -> IO::Fx<std::string> {
perform(Log{.message = "asking"});
auto name = perform(Ask{.prompt = "Name: "});
co_return name;
}Rows can contain other rows — they flatten automatically:
using All = fx::Row<IO, Fail>; // same as Row<Ask, Log, Fail>// All declared effects must be handled — compile error otherwise
auto result = prompt_and_log().run(ask_handler, log_handler);struct StdinAsk : Handler<Ask> {
void handle(Ask e, auto r) {
std::cout << e.prompt;
std::string s; std::getline(std::cin, s);
r(s);
}
};cd tests
make run # run all 5 test files
make check-invalid # verify all 6 invalid test files are correctly rejected
make bench # run all benchmarks (O3)MIT