Crate illicit[][src]

An implicit environment which is indexed by type.

Offering values to the local environment

Add types to the local environment by creating and entering a Layer, and retrieve them using get or expect:

#[derive(Copy, Clone, Debug, PartialEq)]
enum Theme {

// no theme has been set yet

illicit::Layer::new().offer(Theme::Light).enter(|| {
    assert_eq!(*illicit::expect::<Theme>(), Theme::Light,);

// previous theme "expired"

Receiving arguments "by environment"

Annotate functions which require access to types in the environment with from_env:

impl Theme {
    /// Calls `child` with this theme set as current.
    fn set<R>(self, child: impl FnOnce() -> R) -> R {

    /// Retrieves the current theme. Panics if none has been set.
    #[illicit::from_env(current_theme: &Theme)]
    fn current() -> Self {

Theme::Light.set(|| {
    assert_eq!(Theme::current(), Theme::Light);

    // we can set a temporary override for part of our call tree
    Theme::Dark.set(|| assert_eq!(Theme::current(), Theme::Dark));

    // but it only lasts as long as the inner `enter` call
    assert_eq!(Theme::current(), Theme::Light);


This provides convenient sugar for values stored in the current environment as an alternative to thread-locals or a manually propagated context object. However this approach incurs a significant cost in that the following code will panic:

println!("{:?}", Theme::current());

See the attribute's documentation for more details, and please consider whether this is appropriate for your use case before taking it on as a dependency.


Use Snapshot::get to retrieve a copy of the current local environment.



illicit provides capabilities very similar in ways to execution-context. Both crates allow one to propagate implicit values to "downstream" code, and they both allow that downstream code to provide its own additional values to the context. Both crates prevent mutation of contained types without interior mutability.

One notable difference is how they handle multi-threading. execution-context requires types contained in a "flow-local" to implement Send so that contexts can be sent between threads. In contrast, while illicit supports the reuse of contexts, it prioritizes the single-threaded use case and does not require Send.

The other noteworthy difference is in how they're "addressed": execution-context defines static variables that are referenced by name/symbol, whereas illicit allows the definition of a single local value per type and does not rely on naming. This offers some nice properties but it also sacrifices the static guarantee that there will always be a default value.


This crate is implemented on top of a thread-local variable which stores the context, and can be seen as utilities for dynamically creating and destroying thread-locals for arbitrary types. The other key difference is that the ability to temporarily override a thread-local is built-in.

The main cost over writing one's own thread-local is that this does incur the additional overhead of a HashMap access, some TypeId comparison, and some pointer dereferences.



A failure to find a particular type in the local context.


A container for the local environment, usually used to represent a pending addition to it.


A point-in-time representation of the implicit environment.



Implemented by types that can offer themselves as context to a child call.



Returns a reference to a value in the current environment, as get does, but panics if the value has not been set.


Returns a reference to a value in the current environment if it is present.


Removes the provided type from the current environment for the remainder of its scope. Parent environments may still possess a reference to the value.

Attribute Macros


Defines required illicit::get values for a function. Binds the provided types as if references to them were implicit function arguments: