Typed effect handlers for C++20 using coroutines.
corofx is a library for programming with effects and handlers. It uses standard C++20 features only, without external dependencies. The effect semantics are largely inspired by the Koka language.
A useful computer program often needs to perform side effects like I/O operations. However, unrestricted side effects make program behavior difficult to reason about. Effect typing solves this by statically tracking which effects a function can perform, creating a clear separation between pure and effectful computations.
While this concept resembles checked exception specifications, effect handlers make it particularly powerful. Effect handling generalizes exception handling by allowing handlers to resume execution at the call site with a result. This provides some key benefits:
- Similar to dependency injection, effects and their handling logic can be decoupled, improving program modularity and composability.
- Interesting control flow patterns like generators can be implemented without language extensions.
C++ coroutines provide an ideal foundation for building an effect system. Their extensive customization points enable precise control over execution flow. By leveraging coroutines as a native language feature, we can express complex control patterns using standard C++ syntax.
Note
C++ coroutines are state machines that can be resumed only once. As a result, this library supports only one-shot effect handlers.
Here is a generator example adapted from Koka:
#include "corofx/task.hpp"
#include <iostream>
#include <vector>
using namespace corofx;
// Example adapted from https://koka-lang.github.io/koka/doc/book.html#why-handlers.
struct yield {
using return_type = bool;
int i{};
};
auto traverse(std::vector<int> xs) -> task<void, yield> {
for (auto x : xs) {
if (not co_await yield{x}) break;
}
co_return {};
}
auto print_elems() -> task<void> {
co_await traverse(std::vector{1, 2, 3, 4})
.with(handler_of<yield>([](auto&& e, auto&& resume) -> task<void> {
std::cout << "yielded " << e.i << "\n";
co_return resume(e.i <= 2);
}));
co_return {};
}
auto main() -> int { print_elems()(); }
The key components in this example are:
- Effect Definition:
The
yield
effect is a simplestruct
with anint
payload and abool
return type. - Effect Producer:
traverse
returnstask<void, yield>
, indicating it's an effectful computation that:- May produce the
yield
effect. - Returns
void
when complete.
- May produce the
- Effect Handler:
print_elems
discharges theyield
effect fromtraverse
with a handler, which:- Produces no effects1.
- Prints each yielded value.
- Passes a
bool
and transfers control back to the effect producer via tail resumption.
After handling yield
,
print_elems
returns task<void>
- a pure computation returning void
.
Finally, main
simply calls the print_elems
coroutine
since no effects remain to be handled.
When run, this program produces the following output:
yielded 1
yielded 2
yielded 3
Tip
Effect types are checked at compile time, ensuring proper handling throughout the call chain.
Tip
The library uses symmetric transfer to handle repeated effect invocations without stack overflow.
See examples for more interesting use cases of effects and handlers.
A compiler with C++20 support is required. While older compiler versions may work, the following versions are recommended for best results:
Compiler | Version |
---|---|
GCC | 13.3.0 |
Clang | 18.1.3 |
MSVC | 19.42 |
To build and use this library in your project, use CMake with FetchContent
:
cmake_minimum_required(VERSION 3.28)
project(MyProj
LANGUAGES CXX)
include(FetchContent)
FetchContent_Declare(
corofx
GIT_REPOSITORY https://github.com/vchlin/corofx.git
GIT_TAG main
GIT_SHALLOW ON
FIND_PACKAGE_ARGS NAMES CoroFX
)
FetchContent_MakeAvailable(corofx)
add_executable(MyExe main.cpp)
target_link_libraries(MyExe CoroFX::CoroFX)
Footnotes
-
This example still performs I/O by printing to
stdout
. While we could define aconsole
effect to track and handle such operations, C++ doesn't prevent direct I/O operations outside our effect system. ↩