id_effect 0.2.0

Effect<A, E, R> (sync + async), context/layers, pipe — interpreter-style, no bundled executor
Documentation
# Migrating from `async fn` to effects

This appendix is a practical guide for converting existing async Rust code to id_effect. It covers common patterns and their id_effect equivalents, with migration steps for each.

## The Mental Model Shift

In typical async Rust, a function returns a `Future`; when that future is awaited, the work runs:

```rust
async fn get_user(id: u64, db: &DbClient) -> Result<User, DbError> {
    db.query_one("SELECT * FROM users WHERE id = $1", &[&id]).await
}
```

In id_effect, many domain functions return an **`Effect`**—a description you run later with an environment:

```rust
fn get_user<A, E, R>(id: u64) -> Effect<A, E, R>
where
    A: From<User> + 'static,
    E: From<DbError> + 'static,
    R: NeedsDb + 'static,
{
    effect!(|r: &mut R| {
        let db = ~ DbKey;
        let user = ~ db.get_user(id);
        A::from(user)
    })
}
```

The database client is no longer a function parameter. It's declared in `R` and retrieved by the runtime. The business logic is identical; what changes is how dependencies are supplied.

## Pattern 1: async fn → fn returning Effect

**Before**

```rust
pub async fn process_order(
    order_id: OrderId,
    db: &DbClient,
    mailer: &MailClient,
) -> Result<Receipt, AppError> {
    let order = db.get_order(order_id).await?;
    let receipt = db.complete_order(order).await?;
    mailer.send_receipt(&receipt).await?;
    Ok(receipt)
}
```

**After**

```rust
pub fn process_order<A, E, R>(order_id: OrderId) -> Effect<A, E, R>
where
    A: From<Receipt> + 'static,
    E: From<AppError> + 'static,
    R: NeedsDb + NeedsMailer + 'static,
{
    effect!(|r: &mut R| {
        let db     = ~ DbKey;
        let mailer = ~ MailerKey;
        let order   = ~ db.get_order(order_id);
        let receipt = ~ db.complete_order(order);
        ~ mailer.send_receipt(&receipt);
        A::from(receipt)
    })
}
```

**Migration steps:**

1. Remove the dependency parameters (`db`, `mailer`)
2. Add `<A, E, R>` generic parameters
3. Add `where` bounds for each removed dependency
4. Replace `async move { … }` with `effect!(|r: &mut R| { … })`
5. Replace `.await?` with `~ ` prefix
6. Wrap the return value with `A::from(…)`

## Pattern 2: Wrapping Third-Party Async

Third-party libraries return `Future`s, not `Effect`s. Use `from_async` to wrap them:

**Before**

```rust
async fn fetch_price(symbol: &str) -> Result<f64, reqwest::Error> {
    reqwest::get(format!("https://api.example.com/price/{symbol}"))
        .await?
        .json::<PriceResponse>()
        .await
        .map(|r| r.price)
}
```

**After**

```rust
fn fetch_price<A, E, R>(symbol: String) -> Effect<A, E, R>
where
    A: From<f64> + 'static,
    E: From<reqwest::Error> + 'static,
    R: 'static,
{
    from_async(move |_r| async move {
        let price = reqwest::get(format!("https://api.example.com/price/{symbol}"))
            .await?
            .json::<PriceResponse>()
            .await
            .map(|r| r.price)?;
        Ok(A::from(price))
    })
}
```

The `from_async` closure still uses `.await` internally. Only the outermost function signature changes.

## Pattern 3: Error Types

**Before** — single monolithic error enum

```rust
#[derive(Debug)]
enum AppError {
    DbError(DbError),
    MailError(MailError),
    NotFound(String),
}
```

**After** — effects propagate errors through `From` bounds

```rust
// Keep domain errors as-is
#[derive(Debug)] struct NotFoundError(String);

// Effect signatures declare what they can fail with:
fn get_user<A, E, R>(id: u64) -> Effect<A, E, R>
where
    E: From<DbError> + From<NotFoundError> + 'static, // …
```

You still need an `AppError` at the top level (in `main` or your HTTP handler), but individual functions no longer need to know about unrelated error variants.

## Pattern 4: Shared State

**Before** — `Arc<Mutex<T>>` passed through function calls

```rust
async fn handler(state: Arc<Mutex<AppState>>) -> Response {
    let mut s = state.lock().unwrap();
    s.request_count += 1;
    // …
}
```

**After** — shared state in a service, accessed via `R`

```rust
service_key!(AppStateKey: Arc<Mutex<AppState>>);

fn handler<A, E, R>() -> Effect<A, E, R>
where
    R: NeedsAppState + 'static,
    // …
{
    effect!(|r: &mut R| {
        let state = ~ AppStateKey;
        let mut s = state.lock().unwrap();
        s.request_count += 1;
        // …
    })
}
```

Or, for mutable state that needs transactional semantics across fibers, use `TRef`:

```rust
// Replace Arc<Mutex<Counter>> with TRef<u64>
service_key!(CounterKey: TRef<u64>);

fn increment_counter<E, R>() -> Effect<u64, E, R>
where
    R: NeedsCounter + 'static,
    E: 'static,
{
    effect!(|r: &mut R| {
        let counter = ~ CounterKey;
        ~ commit(counter.modify_stm(|n| n + 1));
        ~ commit(counter.read_stm())
    })
}
```

## Pattern 5: Resource Cleanup

**Before** — manual `drop` or relying on `Drop` impls

```rust
async fn with_connection<F, T>(pool: &Pool, f: F) -> Result<T, DbError>
where F: AsyncFnOnce(&Connection) -> Result<T, DbError>
{
    let conn = pool.get().await?;
    let result = f(&conn).await;
    // conn is dropped here — relies on Drop
    result
}
```

**After** — explicit `Scope`

```rust
fn with_connection<A, E, R, F>(f: F) -> Effect<A, E, R>
where
    F: FnOnce(&Connection) -> Effect<A, E, R> + 'static,
    R: NeedsPool + 'static,
    E: From<DbError> + 'static,
    A: 'static,
{
    effect!(|r: &mut R| {
        let pool = ~ PoolKey;
        ~ scope.acquire(
            pool.get(),           // acquire
            |conn| pool.release(conn),  // release (always runs)
            |conn| f(conn),       // use
        )
    })
}
```

The `Scope` finalizer runs whether the inner effect succeeds, fails, or is cancelled. `Drop` doesn't give you that guarantee for async code.

## Migration Strategy

Migrate gradually, one module at a time:

1. Start with leaf functions (those with no id_effect dependencies yet) — convert them first.
2. Move up the call graph. Functions that call converted leaf functions become easy to convert.
3. Push the `run_blocking` call to `main` or the request handler entry point.
4. Convert tests last — once business logic is effect-based, tests become simple layer swaps.

You can mix old-style async functions and effect functions during the transition: wrap async functions with `from_async` and call effect functions with `run_blocking` in async contexts when needed.