Widening and Narrowing — Environment Transformations

Sometimes your effect needs part of an environment, but you have the whole thing. Or you need to thread an effect through a context that provides more than required. This is where zoom_env and contramap_env come in.

The Mismatch Problem

Imagine your application has a large environment type:

#![allow(unused)]
fn main() {
struct AppEnv {
    db: Database,
    logger: Logger,
    config: Config,
    metrics: MetricsClient,
}
}

You have a utility function that only needs a Logger:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
// Adapt log_event to work with AppEnv
let app_log = log_event("hello").zoom_env(|env: &AppEnv| &env.logger);
}

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:

#![allow(unused)]
fn main() {
fn process(data: Data) -> Effect<(), AppError, AppEnv> {
    effect! {
        ~ log_event("start").zoom_env(|e: &AppEnv| &e.logger).map_error(AppError::Log);
        ~ db_query(data).zoom_env(|e: &AppEnv| &e.db).map_error(AppError::Db);
        Ok(())
    }
}
}

contramap_env: Transform the Environment

While zoom_env narrows, contramap_env transforms. It applies a function to convert whatever environment the caller provides into what the effect actually needs:

#![allow(unused)]
fn main() {
// Effect needs a raw string URL
fn connect(url: &str) -> Effect<Database, DbError, String> { ... }

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

contramap_env is the formal name for "adapt the environment type." In practice, most code uses zoom_env for the common case of extracting a field.

R as Documentation Revisited

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

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
// 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 and contramap_env appear most often in library code — when writing reusable utilities that should work with any environment containing the right piece. Application code typically uses Layers and service tags (Chapters 5–6) which avoid the need for explicit projection.

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