Tags — Branding Values with Identity

A Tag is a zero-sized type that acts as a compile-time name for a value. It associates an identifier with a value type, so two values of the same underlying type can be distinguished by their tag.

What Is a Tag?

#![allow(unused)]
fn main() {
use id_effect::{Tag, Tagged, tagged};

// A Tag is a zero-sized type with an associated Value type
struct DatabaseTag;
impl Tag for DatabaseTag {
    type Value = Pool;
}

struct CacheTag;
impl Tag for CacheTag {
    type Value = Pool;  // Same underlying type, different identity
}
}

Tagged<DatabaseTag> is "a Pool identified as the database." Tagged<CacheTag> is "a Pool identified as the cache." They're different types even though both wrap Pool.

Creating Tagged Values

#![allow(unused)]
fn main() {
let db_pool: Pool = connect_database();
let cache_pool: Pool = connect_cache();

// Wrap with identity
let db:    Tagged<DatabaseTag> = tagged(db_pool);
let cache: Tagged<CacheTag>    = tagged(cache_pool);
}

tagged(value) is a simple wrapper constructor. It moves the value inside a Tagged<T> newtype.

To get the value back:

#![allow(unused)]
fn main() {
let pool: &Pool = db.value();
let pool_owned: Pool = db.into_value();
}

Why Tags Make the Compiler Your Friend

Now the swap problem from the previous section becomes a compile error:

#![allow(unused)]
fn main() {
// These are DIFFERENT types
fn needs_database<R: NeedsDatabase>() -> Effect<A, E, R> { ... }
fn needs_cache<R: NeedsCache>() -> Effect<A, E, R> { ... }

// Providing the wrong one fails to compile
effect.provide(tagged::<CacheTag>(pool))
// ERROR: expected Tagged<DatabaseTag>, got Tagged<CacheTag>
}

The compiler distinguishes them. You can't accidentally swap the database and cache connections.

The service_key! Macro

In practice, you don't implement Tag by hand. The service_key! macro generates the boilerplate:

#![allow(unused)]
fn main() {
use id_effect::service_key;

service_key!(DatabaseKey: Pool);
service_key!(CacheKey: Pool);
service_key!(LoggerKey: Logger);
}

Each call creates a tag type with the right Tag implementation. Use these as your service keys.

NeedsX Supertraits

When you write a function that needs a DatabaseKey service, you want the bound expressed cleanly. The NeedsX supertrait pattern does this:

#![allow(unused)]
fn main() {
// Low-level (verbose)
pub fn get_user<R>(id: u64) -> Effect<User, DbError, R>
where
    R: Get<DatabaseKey, Target = Pool>
{ ... }

// High-level (idiomatic) — define NeedsDatabase once
pub trait NeedsDatabase: Get<DatabaseKey, Target = Pool> {}
impl<R: Get<DatabaseKey, Target = Pool>> NeedsDatabase for R {}

// Now use it
pub fn get_user<R: NeedsDatabase>(id: u64) -> Effect<User, DbError, R> { ... }
}

The NeedsX trait is just a named alias for the Get<Key> bound. It makes function signatures readable and allows you to change the key implementation without updating every call site.

Summary

ConceptPurpose
TagZero-sized type acting as a compile-time name
Tagged<T>A value wrapped with a tag identity
tagged(v)Wrap a value with its tag
service_key!(K: V)Macro to generate a tag type
NeedsXSupertrait alias for Get<XKey> bounds

Tags eliminate the position problem. The next section shows how they're assembled into Context — the heterogeneous list that forms the R of a running effect.