The Problem with Positional Types
If you’ve used tuples as R for a while, you’ve probably already hit the wall. Let’s make it explicit.
The Tuple Explosion
Two dependencies: perfectly readable.
Effect<A, E, (Database, Logger)>
Five dependencies: which is which?
Effect<A, E, (Pool, Pool, Logger, Config, HttpClient)>
// ^^^^ two Pools — which is the main DB and which is the cache?
Tuples are positional. (Pool, Pool, ...) is ambiguous — both fields have the same type. There’s no way to distinguish them except by index, and index-based access is error-prone and breaks silently when you reorder the tuple.
The Fragility Problem
Positional types are fragile under change. Say your function started with:
fn foo() -> Effect<A, E, (Database, Logger)>
// 0 1
Now a teammate adds Config between them:
fn foo() -> Effect<A, E, (Database, Config, Logger)>
// 0 1 2
Every caller that was providing a tuple (db, log) must be updated to (db, config, log). The change in position is invisible to the type system — the compiler won’t tell you where the old index references are. It’s a silent bomb.
The Same-Type Collision
The deeper problem: Rust can’t distinguish Pool for the main database from Pool for the cache. They’re the same type. Positional tuples just accept both:
// V1: run with (main_pool, cache_pool)
// V2: accidentally swap them
run_blocking(effect, (cache_pool, main_pool)) // compiles, wrong at runtime
No compile error. Wrong behaviour. Possibly wrong for months before you notice.
What We Actually Need
We need a way to give each dependency a name — a compile-time identifier that’s independent of its type and its position in any list.
What if:
Databasemeant “the tagged Pool known as DatabaseTag”Cachemeant “the tagged Pool known as CacheTag”
Then you couldn’t accidentally swap them — they’d be different types even though both are Pool underneath.
That’s exactly what Tags provide.