Providing Dependencies — run_with and Env

An effect with R = caps!(…) cannot run until its capabilities exist. Provide at the edge, not inside library code.

run_with — the main entrypoint

use id_effect::{Effect, ProviderSpecDerive, caps, effect, provide, require, run_with, succeed};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Counter(pub u32);

#[derive(ProviderSpecDerive)]
#[provides(Counter)]
struct CounterLive;

impl CounterLive {
    fn new() -> Counter {
        Counter(42)
    }
}

fn app() -> Effect<u32, (), caps!(Counter)> {
    effect!(|r| {
        let counter = ~Counter;
        counter.0
    })
}

fn main() {
    let n = run_with([provide!(CounterLive)], app()).expect("run");
}

provide! wraps a ProviderSpec as a ProviderBox. run_with collects providers, plans build order via CapabilityGraph, and runs the effect.

Multiple providers

Pass every provider the app needs in one list — order in the array does not matter; the graph topologically sorts by requires():

#![allow(unused)]
fn main() {
run_with(
    [
        provide!(ConfigLive),
        provide!(DatabaseLive),
        provide!(LoggerLive),
        provide!(UserRepoLive),
    ],
    my_application(),
)?;
}

Manual Env for tests

When you only need a handful of values, build Env by hand:

#![allow(unused)]
fn main() {
let mut env = Env::new();
env.insert::<Cap<Database>>(mock_pool);
env.insert::<Cap<EffectLogger>>(test_logger);

let user = run_blocking(get_user(42), env)?;
}

build_env is the middle ground — same provider types as production, but you get the Env back without running an effect:

#![allow(unused)]
fn main() {
let env = build_env([provide!(MockUserRepoLive)])?;
run_test(get_user(1), env)?;
}

Where to provide

Provide at main, test setup, or HTTP/Tokio boundaries — not inside library functions.

#![allow(unused)]
fn main() {
// BAD — library reaches for concrete deps
pub fn process_order(order: Order) -> Effect<Receipt, AppError, ()> {
    let db = connect("hardcoded-url");
    // ...
}

// GOOD — library declares needs; caller wires them
pub fn process_order(order: Order) -> Effect<Receipt, AppError, caps!(Database, EffectLogger)> {
    // ...
}
}

Summary

APIPurpose
run_with([provide!(P), …], effect)Build graph + run
build_env([provide!(P), …])Build Env only
Env::insert::<Cap<K>>(value)Manual test wiring
run_blocking(effect, env)Run when Env is already built

None of these execute effect steps until run_with / run_blocking / run_async is called — wiring stays lazy until the boundary.