Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Widening and Narrowing — Environment Transformations

Sometimes your effect needs part of an environment, but the caller has the whole thing. This is where zoom_env comes in.

The Mismatch Problem

Imagine your application has a large environment type:

struct AppEnv {
    db: Database,
    logger: Logger,
    config: Config,
    metrics: MetricsClient,
}

You have a utility function that only needs a Logger:

fn log_event(msg: &str) -> Effect<(), LogError, Logger> { ... }

You can’t call this inside an effect! block that has AppEnv in scope — the types don’t match. You need to narrow the environment down.

zoom_env: Narrow the Environment

zoom_env adapts an effect to work with a larger environment by providing a lens from the larger type to the smaller one:

// Adapt log_event to work with AppEnv.
// The projection returns the smaller environment by value.
let app_log = log_event("hello").zoom_env(|env: &mut AppEnv| env.logger.clone());

Now app_log has type Effect<(), LogError, AppEnv>. The function extracts the Logger from AppEnv and feeds it to the original effect.

Inside effect!, the pattern looks like:

fn process(data: Data) -> Effect<(), AppError, AppEnv> {
    effect! {
        bind* log_event("start").zoom_env(|e: &mut AppEnv| e.logger.clone()).map_error(AppError::Log);
        bind* db_query(data).zoom_env(|e: &mut AppEnv| e.db.clone()).map_error(AppError::Db);
    }
}

Transforming the Environment

The current API uses zoom_env for both narrowing and transformation. It applies a function to convert the caller’s environment into what the effect needs:

// Effect needs a raw string URL
fn connect_raw() -> Effect<Database, DbError, String> { ... }

// You have a Config that contains the URL
let with_config = connect_raw().zoom_env(|cfg: &mut Config| cfg.db_url.clone());
// Now type is Effect<Database, DbError, Config>

There is no separate contramap_env method in the current API.

R as Documentation Revisited

These combinators highlight why R is valuable as documentation. When you see:

fn log_event(msg: &str) -> Effect<(), LogError, Logger>

You know exactly what this function needs. You don’t need to read its body to see if it also touches the database. The zoom_env call at the use site makes the adaptation explicit — it’s not hidden.

Compare to the pre-effect alternative:

// Traditional: you'd need to read the body to know what `env` is used for
fn log_event(env: &AppEnv, msg: &str) -> Result<(), LogError> { ... }

With R, the function declares what it needs. With zoom_env, the caller declares how to satisfy it.

When to Use These

In practice, zoom_env appears most often in library code — when writing reusable utilities that should work with any environment containing the right piece. Application code often uses ServiceContext, Context, and Layers instead.

Think of zoom_env as the manual fallback when the automatic layer-based wiring isn’t the right fit.