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
| Situation | Use |
|---|---|
| Fixed compile-time HList; path-sensitive internals | Context |
| Derive-service app / layer code; runtime wiring | ServiceContext |
| Bridging the two at the composition root | ctx! → .into_service_context() |