corophage 0.3.0

Algebraic effects for stable Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# Corophage

[![Crates.io](https://img.shields.io/crates/v/corophage.svg)](https://crates.io/crates/corophage)
[![Docs.rs](https://docs.rs/corophage/badge.svg)](https://docs.rs/corophage)
![Stable Rust](https://img.shields.io/badge/rust-stable-orange)
[![Coverage](https://codecov.io/github/romac/corophage/graph/badge.svg?token=U8FVD3HT2X)](https://codecov.io/github/romac/corophage)

**Algebraic effects for stable Rust**

`corophage` provides a way to separate the *description* of what your program should do from the *implementation* of how it gets done. This allows you to write clean, testable, and composable business logic.

## Usage

Add `corophage` to your `Cargo.toml`:

```toml
[dependencies]
corophage = "0.2.0"
```

## What are effect handlers?

Imagine you are writing a piece of business logic:

1.  Log a "starting" message.
2.  Read some configuration from a file.
3.  Get the current application state.
4.  Perform a calculation and update the state.
5.  If a condition is met, cancel the entire operation.

Traditionally, you might write a function that takes a logger, a file system handle, and a mutable reference to the state. This function would be tightly coupled to these specific implementations.

With effect handlers, your business logic function does none of these things directly. Instead, it *describes* the side effects it needs to perform by `yield`ing **effects**.

```rust,ignore
use corophage::prelude::*;

type MyEffects = Effects![Log, FileRead, GetState, SetState];

// This describes WHAT to do, not HOW.
let result = Program::new(|y: Yielder<'_, MyEffects>| async move {
    y.yield_(Log("Starting...")).await;
    let config = y.yield_(FileRead("config.toml")).await;
    let state = y.yield_(GetState).await;
    // ...and so on
})
.handle(|Log(msg)| { println!("{msg}"); Control::resume(()) })
.handle(|FileRead(f)| Control::resume(std::fs::read_to_string(f).unwrap()))
.handle(|_: GetState| Control::resume(42u64))
.handle(|SetState(x)| { /* ... */ Control::resume(()) })
.run_sync();
```

The responsibility of *implementing* these effects (e.g., actually printing to the console, reading from the disk, or managing state) is given to a set of **handlers**. You provide these handlers to a runner, which executes your logic and calls the appropriate handler whenever an effect is yielded.

This separation provides powerful benefits:
*   **Testability**: For tests, you can provide mock handlers that simulate logging, file I/O, or state management without touching the real world.
*   **Modularity**: The core logic is completely decoupled from its execution context. You can run the same logic with different handlers for different environments (e.g., production vs. testing, CLI vs. GUI).
*   **Clarity**: The business logic code becomes a pure, high-level description of the steps involved, making it easier to read and reason about.

> [!NOTE]
> `corophage` provides **single-shot** effect handlers: each handler can resume the computation at most once. This means handlers cannot duplicate or replay continuations. This is a deliberate design choice that keeps the implementation efficient and compatible with Rust's ownership model.

## Core concepts

`corophage` is built around a few key concepts: Effects, Computations, and Handlers.

### 1. Effects

An **Effect** is a struct that represents a request for a side effect. It's a message from your computation to the outside world. To define an effect, you implement the `Effect` trait.

The most important part of the `Effect` trait is the associated type `Resume<'r>` (a generic associated type), which defines the type of the value that the computation will receive back after the effect is handled.

```rust,ignore
use corophage::{Effect, Never};

// An effect to request logging a message.
// It doesn't need any data back, so we resume with `()`.
pub struct Log<'a>(pub &'a str);
impl<'a> Effect for Log<'a> {
    type Resume<'r> = ();
}

// An effect to request reading a file.
// It expects the file's contents back, so we resume with `String`.
pub struct FileRead(pub String);
impl Effect for FileRead {
    type Resume<'r> = String;
}

// An effect that cancels the computation.
// It will never resume, so we use the special `Never` type.
pub struct Cancel;
impl Effect for Cancel {
    type Resume<'r> = Never;
}
```

You can also use the `#[effect]` attribute macro to derive the `Effect` impl:

```rust,ignore
use corophage::prelude::*;

#[effect(())]
pub struct Log<'a>(pub &'a str);

#[effect(String)]
pub struct FileRead(pub String);

#[effect(Never)]
pub struct Cancel;

// The resume type may reference the GAT lifetime `'r`:
#[effect(&'r str)]
pub struct Lookup(pub String);

// Generics work too:
#[effect(T)]
pub struct Identity<T: Debug + Send + Sync>(pub T);
```

Or use the `declare_effect!` macro for a more concise syntax:

```rust,ignore
use corophage::prelude::*;

declare_effect!(Log(String) -> ());
declare_effect!(FileRead(String) -> String);
declare_effect!(Cancel -> Never);

// Lifetime and generic parameters are also supported:
declare_effect!(Borrow<'a>(&'a str) -> bool);
declare_effect!(Generic<T: std::fmt::Debug>(T) -> T);

// Named fields are also supported:
declare_effect!(FileRead { path: String, recursive: bool } -> Vec<u8>);

// The resume type may reference the GAT lifetime `'r`:
declare_effect!(Lookup(String) -> &'r str);
```

### 2. Programs

A **Program** combines a computation with its effect handlers. The simplest way to create one is with the `#[effectful]` attribute macro:

```rust,ignore
use corophage::prelude::*;

#[effectful(Log<'static>, FileRead)]
fn fetch_data() -> String {
    yield_!(Log("fetching..."));
    yield_!(FileRead("data.txt".to_string()))
}

let result = fetch_data()
    .handle(|Log(msg)| { println!("{msg}"); Control::resume(()) })
    .handle(|FileRead(path)| Control::resume(format!("contents of {path}")))
    .run_sync();

assert_eq!(result, Ok("contents of data.txt".to_string()));
```

The `#[effectful]` macro transforms your function to return a `Program` and lets you use `yield_!(effect)` to perform effects. You can also create programs manually with `Program::new`:

```rust,ignore
use corophage::prelude::*;

type Effs = Effects![Log<'static>, FileRead];

let result = Program::new(|y: Yielder<'_, Effs>| async move {
    y.yield_(Log("fetching...")).await;
    y.yield_(FileRead("data.txt".to_string())).await
})
.handle(|Log(msg)| { println!("{msg}"); Control::resume(()) })
.handle(|FileRead(path)| Control::resume(format!("contents of {path}")))
.run_sync();

assert_eq!(result, Ok("contents of data.txt".to_string()));
```

When you call `yield_!` (or `y.yield_(...).await` in the manual style), the computation pauses, the effect is handled, and execution resumes with the value provided by the handler.

> [!IMPORTANT]
> Handlers must be attached in the same order as the effects appear in the `Effects![...]` list. This is enforced by the type system — attaching handlers in the wrong order is a compile error.

### 3. Handlers

A **Handler** is a function (sync or async) that implements the logic for a specific effect. It receives the `Effect` instance and returns a `Control<R>` (where `R` is the effect's `Resume` type), which tells the runner what to do next:

*   `Control::resume(value)`: Resumes the computation, passing `value` back as the result of the `yield_`. The type of `value` must match the effect's `Resume` type.
*   `Control::cancel()`: Aborts the entire computation immediately. The final result of the run will be `Err(Cancelled)`.

```rust,ignore
// Sync handler — a regular closure
|Log(msg)| {
    println!("LOG: {msg}");
    Control::resume(())
}

// Async handler — an async closure
async |FileRead(file)| {
    let content = tokio::fs::read_to_string(file).await.unwrap();
    Control::resume(content)
}
```

### 4. Shared state

Handlers can share mutable state via `run_sync_stateful` / `run_stateful`. The state is passed as a `&mut S` first argument to every handler:

```rust,ignore
let mut count: u64 = 0;

let result = Program::new(|y: Yielder<'_, Effects![Counter]>| async move {
    let a = y.yield_(Counter).await;
    let b = y.yield_(Counter).await;
    a + b
})
.handle(|s: &mut u64, _: Counter| {
    *s += 1;
    Control::resume(*s)
})
.run_sync_stateful(&mut count);

assert_eq!(result, Ok(3));
assert_eq!(count, 2);
```

> [!NOTE]
> If your handlers don't need shared state, use `.run_sync()` / `.run()` instead. You can also use `RefCell` or other interior mutability patterns to share state without `run_stateful`.

### 5. Program composition

Programs can invoke other programs. The sub-program's effects must be a subset of the outer program's effects — each yielded effect is forwarded to the outer handler automatically.

```rust,ignore
use corophage::prelude::*;

#[effect(&'static str)]
struct Ask(&'static str);

#[effect(())]
struct Print(String);

#[effect(())]
struct Log(&'static str);

#[effectful(Ask, Print)]
fn greet() {
    let name: &str = yield_!(Ask("name?"));
    yield_!(Print(format!("Hello, {name}!")));
}

#[effectful(Ask, Print, Log)]
fn main_program() {
    yield_!(Log("Starting..."));
    invoke!(greet());
    yield_!(Log("Done!"));
}

let result = main_program()
    .handle(|_: Ask| Control::resume("world"))
    .handle(|Print(msg)| { println!("{msg}"); Control::resume(()) })
    .handle(|_: Log| Control::resume(()))
    .run_sync();

assert_eq!(result, Ok(()));
```

With the manual `Program::new` API, use `y.invoke(program).await`:

```rust,ignore
let result = Program::new(|y: Yielder<'_, Effects![Ask, Print, Log]>| async move {
    y.yield_(Log("Starting...")).await;
    y.invoke(greet()).await;
    y.yield_(Log("Done!")).await;
})
.handle(|_: Ask| Control::resume("world"))
.handle(|Print(msg)| { println!("{msg}"); Control::resume(()) })
.handle(|_: Log| Control::resume(()))
.run_sync();
```

Sub-programs can be nested arbitrarily — a sub-program can itself invoke other sub-programs.

## Advanced: `Co`, `CoSend`, and the direct API

For cases where you need to pass a computation around before attaching handlers (e.g., returning it from a function, or storing it in a data structure), you can use `Co` and `CoSend` directly.

```rust,ignore
use corophage::{Co, CoSend, sync, Control};
use corophage::prelude::*;

// Co — the computation type (not Send)
let co: Co<'_, Effects![FileRead], String> = Co::new(|y| async move {
    y.yield_(FileRead("data.txt".to_string())).await
});

// Run directly with all handlers at once via hlist
let result = sync::run(co, &mut hlist![
    |FileRead(f)| Control::resume(format!("contents of {f}"))
]);

// Or wrap in a Program for incremental handling
let result = Program::from_co(co).handle(/* ... */).run_sync();
```

`CoSend` is the `Send`-able variant, for use with multi-threaded runtimes:

```rust,ignore
fn my_computation() -> CoSend<'static, Effects![FileRead], String> {
    CoSend::new(|y| async move {
        y.yield_(FileRead("test".to_string())).await
    })
}

// Can be spawned on tokio
tokio::spawn(async move {
    let result = Program::from_co(my_computation())
        .handle(async |FileRead(f)| Control::resume(format!("contents of {f}")))
        .run()
        .await;
});
```

The direct API (`sync::run`, `sync::run_stateful`, `asynk::run`, `asynk::run_stateful`) accepts a `Co`/`CoSend` and an `hlist!` of all handlers at once. This is useful for concise one-shot execution but requires providing all handlers together.

#### Borrowing non-`'static` data

Effects can borrow data from the local scope by using a non-`'static` lifetime:

```rust,ignore
use corophage::prelude::*;

struct Log<'a>(pub &'a str);
impl<'a> Effect for Log<'a> {
    type Resume<'r> = ();
}

let msg = String::from("hello from a local string");
let msg_ref = msg.as_str();

let result = Program::new(move |y: Yielder<'_, Effects![Log<'_>]>| async move {
    y.yield_(Log(msg_ref)).await;
})
.handle(|Log(m)| { println!("{m}"); Control::resume(()) })
.run_sync();

assert_eq!(result, Ok(()));
```

#### Borrowed resume types

Because `Effect::Resume<'r>` is a generic associated type (GAT), handlers can resume computations with *borrowed* data instead of requiring owned values.

```rust,ignore
use corophage::prelude::*;
use std::collections::HashMap;

struct Lookup<'a> {
    map: &'a HashMap<String, String>,
    key: &'a str,
}

impl<'a> Effect for Lookup<'a> {
    // The handler can resume with a &str borrowed from the map
    type Resume<'r> = &'r str;
}

let map = HashMap::from([
    ("host".to_string(), "localhost".to_string()),
    ("port".to_string(), "5432".to_string()),
]);

let result = Program::new({
    let map = &map;
    move |y: Yielder<'_, Effects![Lookup<'_>]>| async move {
        let host: &str = y.yield_(Lookup { map, key: "host" }).await;
        let port: &str = y.yield_(Lookup { map, key: "port" }).await;
        format!("{host}:{port}")
    }
})
.handle(|Lookup { map, key }| {
    let value = map.get(key).unwrap();
    Control::resume(value.as_str())
})
.run_sync();

assert_eq!(result, Ok("localhost:5432".to_string()));
```

## Performance

Benchmarks were run using [Divan](https://github.com/nvzqz/divan). Run them with `cargo bench`.

### Coroutine Overhead

| Benchmark | Median | Notes |
|-----------|--------|-------|
| `coroutine_creation` | ~7 ns | Just struct initialization |
| `empty_coroutine` | ~30 ns | Full lifecycle with no yields |
| `single_yield` | ~38 ns | One yield/resume cycle |

Coroutine creation is nearly free, and the baseline overhead for running a coroutine is ~30 ns.

### Yield Scaling (Sync vs Async)

| Yields | Sync | Async | Overhead |
|--------|------|-------|----------|
| 10 | 131 ns | 178 ns | +36% |
| 100 | 1.0 µs | 1.27 µs | +27% |
| 1000 | 9.5 µs | 11.1 µs | +17% |

Async adds ~30% overhead at small scales, but the gap narrows as yield count increases. Per-yield cost is approximately **9-10 ns** for sync and **11 ns** for async.

### Effect Dispatch Position

| Position | Median |
|----------|--------|
| First (index 0) | 49 ns |
| Middle (index 2) | 42 ns |
| Last (index 4) | 47 ns |

Dispatch position has negligible impact. While the source-level dispatch uses recursive trait impls over nested `Coproduct::Inl`/`Inr` variants, the compiler monomorphizes and inlines the entire chain into a flat discriminant-based branch — the same code LLVM would emit for a plain `match` on a flat enum. The result is effectively O(1).

### State Management

| Pattern | Median |
|---------|--------|
| Stateless (`run`) | 38 ns |
| Stateful (`run_stateful`) | 53 ns |
| RefCell pattern | 55 ns |

Stateful handlers add ~15 ns overhead. RefCell is nearly equivalent to `run_stateful`.

### Handler Complexity

| Handler | Median |
|---------|--------|
| Noop (returns `()`) | 42 ns |
| Allocating (returns `String`) | 83 ns |

Allocation dominates handler cost. Consider returning references or zero-copy types for performance-critical effects.


## Acknowledgments

`corophage` is heavily inspired by [`effing-mad`](https://github.com/rosefromthedead/effing-mad), a pioneering algebraic effects library for nightly Rust. 
`effing-mad` demonstrated that algebraic effects and effect handlers are viable in Rust by leveraging coroutines to let effectful functions suspend, pass control to their callers, and resume with results.
While `effing-mad` requires nightly Rust for its `#[coroutine]`-based approach, `corophage` supports stable Rust by leveraging async coroutines (via [`fauxgen`](https://github.com/jmkr/fauxgen)). Big thanks as well to [`frunk`](https://github.com/lloydmeta/frunk) for its coproduct and hlist implementation.

## License

Licensed under either of

 * Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>)
 * MIT license (<http://opensource.org/licenses/MIT>)

at your option.

## Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.