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

Context and HLists — The Heterogeneous Stack

Context<L> wraps a heterogeneous list of typed cells. In the tagged API, each cell is usually Tagged<K, V>.

The Structure: Cons / Nil

use effectful::{Cons, Nil, Tagged};

type Empty = Nil;
type WithDb = Cons<Tagged<DatabaseKey, Pool>, Nil>;
type WithDbAndLogger = Cons<Tagged<DatabaseKey, Pool>, Cons<Tagged<LoggerKey, Logger>, Nil>>;

Cons<Head, Tail> prepends one item to a list. Nil is the empty list.

Building Context Values

use effectful::{Cons, Context, Nil, tagged};

let env = Context::new(Cons(
    tagged::<DatabaseKey, _>(my_pool),
    Cons(tagged::<LoggerKey, _>(my_logger), Nil),
));

You can also prepend to an existing context:

let base = Context::new(Cons(tagged::<LoggerKey, _>(my_logger), Nil));
let full = base.prepend(tagged::<DatabaseKey, _>(my_pool));

Access

Context::get::<K>() reads the head cell when it has key K.

let pool: &Pool = full.get::<DatabaseKey>();

For non-head cells, use get_path::<K, P>() with an explicit type-level path such as ThereHere / Skip1.

let logger: &Logger = full.get_path::<LoggerKey, ThereHere>();

This explicit path is why application code often wraps bounds in NeedsX traits or uses ServiceContext instead.

Why HLists and Not HashMap?

An HList preserves each value’s type in the environment type. That gives compile-time lookup and no runtime downcast. The cost is verbose types like Cons<Tagged<A, V>, Cons<Tagged<B, W>, Nil>>.

Use HList Context when you want maximum static structure. Use ServiceContext when you want a simpler runtime service table keyed by service type.

Converting Context to ServiceContext

At the composition root you may have a statically-typed Context but need to hand it to code that expects ServiceContext. Use [IntoServiceContext]:

use effectful::{ctx, Effect, IntoServiceContext, MissingService, Service, ServiceContext,
  run_blocking};

#[derive(Clone, Hash, Service)]
struct Config { port: u16 }

let static_ctx = ctx!(Config => Config { port: 8080 });
let runtime_ctx: ServiceContext = static_ctx.into_service_context();

let program: Effect<u16, MissingService, ServiceContext> =
  Config::use_sync(|config| config.port);

assert_eq!(run_blocking(program, runtime_ctx), Ok(8080));

Only self-keyed service cells (Tagged<S, S> where S implements [Service]) convert. Arbitrary tagged values cannot silently enter runtime service lookup. Duplicate service types in one list make the head cell win, matching compile-time lookup intuition.

When to Use Which

SituationUse
Fixed compile-time HList; path-sensitive internalsContext
Derive-service app / layer code; runtime wiringServiceContext
Bridging the two at the composition rootctx!.into_service_context()