Rust macros that allow for Scala-like for comprehensions and a Chest type that is like both Option and Result rolled into one.
Find a file
David Pollak bb8bed58bf
Did rename to cada
Signed-off-by: David Pollak <feeder.of.the.bears@gmail.com>
2026-02-08 16:55:19 -05:00
examples Did rename to cada 2026-02-08 16:55:19 -05:00
src Did rename to cada 2026-02-08 16:55:19 -05:00
tests Did rename to cada 2026-02-08 16:55:19 -05:00
.gitignore First commit 2026-02-08 16:30:24 -05:00
ARCHITECTURE.md Did rename to cada 2026-02-08 16:55:19 -05:00
Cargo.lock Did rename to cada 2026-02-08 16:55:19 -05:00
Cargo.toml Did rename to cada 2026-02-08 16:55:19 -05:00
CHANGELOG.md Did rename to cada 2026-02-08 16:55:19 -05:00
CONTRIBUTING.md Did rename to cada 2026-02-08 16:55:19 -05:00
LICENSE-APACHE First commit 2026-02-08 16:30:24 -05:00
LICENSE-MIT Did rename to cada 2026-02-08 16:55:19 -05:00
PUBLISHING.md Did rename to cada 2026-02-08 16:55:19 -05:00
README.md Did rename to cada 2026-02-08 16:55:19 -05:00
TROUBLESHOOTING.md Did rename to cada 2026-02-08 16:55:19 -05:00

cada

The Problem

Multi-step operations in Rust turn into deeply nested match and if let chains that are painful to read and painful to maintain:

fn process_order(db: &Database, order_id: i32) -> Option<String> {
    if let Some(order) = db.find_order(order_id) {
        if let Some(user) = db.find_user(order.user_id) {
            if let Some(address) = db.find_address(user.address_id) {
                if address.country == "US" {
                    if let Some(tax) = db.tax_rate(&address.state) {
                        Some(format!(
                            "Ship to {} at {}% tax",
                            user.name,
                            tax * 100.0
                        ))
                    } else { None }
                } else { None }
            } else { None }
        } else { None }
    } else { None }
}

Every new step pushes you one level deeper. The happy path is buried under boilerplate, and adding error context means even more nesting.

The Solution

cada! flattens this into a clean, linear pipeline:

use cada::cada;

fn process_order(db: &Database, order_id: i32) -> Option<String> {
    cada! {
        order   <- db.find_order(order_id);
        user    <- db.find_user(order.user_id);
        address <- db.find_address(user.address_id);
        guard address.country == "US";
        tax     <- db.tax_rate(&address.state);
        yield format!("Ship to {} at {}% tax", user.name, tax * 100.0)
    }
}

Each step either produces a value (<-) or filters (guard). If any step fails, the whole thing short-circuits — no nesting, no else { None }. It works with Option, Result, Vec, and more. No new types required.

Borrow checker friendly

Because the macro expands to nested for-loops (not closures), variables from earlier bindings are directly in scope for later ones. No fighting with lifetimes or move:

use cada::prelude::*;
use cada::cada;

fn get_name() -> Chest<String> { Full("Alice".to_string()) }
fn get_suffix(name: &str) -> Chest<String> { Full(format!("{}!", name)) }
fn validate(a: &str, b: &str) -> Chest<String> { Full(format!("{}-{}", a, b)) }

let result: Chest<String> = cada! {
    a <- get_name();
    b <- get_suffix(&a);        // borrows a
    c <- validate(&a, &b);      // borrows both a and b
    yield format!("{}{}: {}", a, b, c)
};

Try doing that with .and_then(|a| ...) chains — the borrow checker will fight you every step of the way.

The Bonus: Chest<T>

When Option and Result aren't enough — you need to distinguish "not found" from "something broke", attach error codes, or chain failure context — Chest<T> fills the gap. And it works seamlessly inside cada!:

use cada::prelude::*;
use cada::cada;

fn find_user(id: i32) -> Chest<String> {
    if id == 1 { Full("Alice".to_string()) } else { Chest::Empty }
}

let result: Chest<String> = cada! {
    user <- find_user(99) ?~ "user not found", 404u16;
    yield user
};

assert!(result.is_failure());
assert_eq!(result.failure_msg(), Some("user not found"));
assert_eq!(result.failure_param::<u16>(), Some(&404));

Chest<T> has three states: Full(T) (value present), Empty (absent, no error), and Failure(FailureInfo) (absent, with error context). The ?~ syntax annotates why something failed, so you get rich diagnostics without writing error-handling boilerplate.

When to Use What

Situation Use
Value may be absent, no error info needed Option
Operation can fail, one error type Result
Absent vs failed matters, or you need chained failures / typed params Chest

cada! vs .map/.and_then chains:

Use cada! when you have:

  • More than 2 steps chained together
  • Guards (filtering conditions) mixed in
  • Multiple source types mixed (e.g. Vec + Option)
  • Borrow-checker friction from closures borrowing earlier values

For simple 1-2 step chains, .map and .and_then are fine.

Quick Start

Add to your Cargo.toml:

[dependencies]
cada = "0.1"
use cada::prelude::*;
use cada::cada;

// Chest basics
let name: Chest<String> = Full("Alice".to_string());
let greeting = name.map(|n| format!("Hello, {}!", n));
assert_eq!(greeting, Full("Hello, Alice!".to_string()));

// For-comprehension with Chest
let result: Chest<String> = cada! {
    user <- Full("Alice".to_string());
    age  <- Full(30);
    guard age >= 18;
    yield format!("{} (age {})", user, age)
};
assert_eq!(result, Full("Alice (age 30)".to_string()));

// For-comprehension with Vec (cross product)
let pairs: Vec<(i32, i32)> = cada! {
    x <- vec![1, 2, 3];
    y <- vec![10, 20];
    yield (x, y)
};
assert_eq!(pairs, vec![(1, 10), (1, 20), (2, 10), (2, 20), (3, 10), (3, 20)]);

Runnable Examples

The examples/ directory contains working programs you can run immediately:

# Chest basics: Full, Empty, Failure, monadic operations
cargo run --example basic

# cada! macro: Option, Vec, Chest, guards, failure annotation
cargo run --example comprehension

# JSON querying with typed extraction (requires serde_json feature)
cargo run --example json_query --features serde_json

Chest<T> In Depth

Chest<T> is an enum with three variants:

enum Chest<T> {
    Full(T),     // Value present
    Empty,       // Value absent
    Failure(FailureInfo),  // Value absent, with error context
}

Why not just Option/Result?

Option has no error context. Result forces you to pick a single error type. Chest gives you:

  • Empty vs Failure -- distinguish "nothing here" from "something went wrong"
  • Failure chaining -- build a chain of failure messages with compound_fail_msg
  • Typed failure parameters -- attach HTTP status codes, error codes, etc. via with_param
  • Runtime type inspection -- chest_create! + as_a::<T>() for type-erased pipelines

Monadic operations

use cada::prelude::*;

let chest = Full(21);

// map, and_then (flatMap), filter
let doubled = chest.map(|x| x * 2);                   // Full(42)
let chained = Full(10).and_then(|x| Full(x + 5));     // Full(15)
let filtered = Full(42).filter(|x| *x > 0);           // Full(42)
let gone = Full(-1).filter(|x| *x > 0);               // Empty

// Extraction
let val = Full(42).open_or(0);                         // 42
let val = Chest::<i32>::Empty.open_or(99);             // 99
let val = Full(42).open_or_else(|| expensive_default()); // 42

Failure annotation

use cada::prelude::*;

// Convert Empty to Failure with a message
let chest: Chest<i32> = Chest::Empty;
let annotated = chest.fail_msg("value required");
assert!(annotated.is_failure());
assert_eq!(annotated.failure_msg(), Some("value required"));

// Chain failures
let inner: Chest<i32> = Chest::Failure(FailureInfo::new("not found"));
let outer = inner.compound_fail_msg("lookup failed");
// failure_chain() returns the chain of FailureInfo refs

// Attach typed parameters (e.g. HTTP status codes)
let chest: Chest<i32> = Chest::Failure(FailureInfo::new("not found"));
let with_code = chest.with_param(404u16);
assert_eq!(with_code.failure_param::<u16>(), Some(&404));

Runtime type inspection

use cada::prelude::*;
use cada::chest_create;

// chest_create! wraps a value in Chest<TypedValue>, preserving runtime type info
let chest = chest_create!(42i32);

// Inspect and downcast
assert!(chest.is_a::<i32>());
assert!(!chest.is_a::<String>());

let as_int: Chest<i32> = chest.as_a::<i32>();     // Full(42)

let chest2 = chest_create!("hello".to_string());
let as_str: Chest<String> = chest2.as_a::<String>(); // Full("hello")

Conversions

use cada::prelude::*;

// From/Into Option
let chest: Chest<i32> = Some(42).into();    // Full(42)
let opt: Option<i32> = Full(42).into();     // Some(42)

// From/Into Result
let chest: Chest<i32> = Ok::<_, std::io::Error>(42).into();  // Full(42)
let result: Result<i32, FailureInfo> = Full(42).into();       // Ok(42)

// To Vec (0 or 1 elements)
let items = Full(42).to_vec();    // vec![42]

// Catch panics
let safe = tryo(|| might_panic());          // Full(result) or Failure
let safe = tryo_result(fallible_call());    // Result -> Chest

cada! Macro

The cada! macro provides Haskell-style for-comprehensions. It desugars to nested for-loops with no closures, so the borrow checker sees flat scoping and variables from earlier bindings can be freely referenced later.

Syntax

cada! {
    pattern <- source;                           // bind
    pattern <- source ?~ "error message";        // bind with failure annotation
    pattern <- source ?~ "error msg", param;     // bind with message + typed param
    guard condition;                             // filter
    guard condition, ?~ "error message";         // filter with failure annotation
    guard condition, ?~ "error msg", param;      // filter with message + typed param
    let pattern = expression;                    // local binding
    yield expression                             // final value (no trailing semicolon)
}

The first binding's type determines the output type.

What Happens When Things Fail

Understanding short-circuit behavior is key to using cada! effectively.

Empty propagation — if any binding produces no value, the rest is skipped:

use cada::cada;

let result: Option<i32> = cada! {
    x <- Some(10);
    y <- None::<i32>;       // short-circuits here
    yield x + y             // never reached
};
assert_eq!(result, None);

Failure annotation with ?~ — turns an empty/failed binding into a Failure with a message (only meaningful for Chest and Result output):

use cada::prelude::*;
use cada::cada;

let result: Chest<i32> = cada! {
    x <- Full(10);
    y <- Chest::<i32>::Empty ?~ "y was missing", 400u16;
    yield x + y
};

assert!(result.is_failure());
assert_eq!(result.failure_msg(), Some("y was missing"));
assert_eq!(result.failure_param::<u16>(), Some(&400));

Guard failure — if a guard is false, the current iteration is skipped. For monadic types this means empty output; for collections it filters that item:

use cada::cada;

// Monadic: guard failure → empty result
let result: Option<i32> = cada! {
    x <- Some(10);
    guard x > 100;          // false → short-circuit
    yield x
};
assert_eq!(result, None);

// Collection: guard failure → item filtered out
let result: Vec<i32> = cada! {
    x <- vec![1, 2, 3, 4, 5];
    guard x % 2 == 0;       // keeps only even numbers
    yield x
};
assert_eq!(result, vec![2, 4]);

With Option

use cada::cada;

let result: Option<String> = cada! {
    name <- Some("Alice".to_string());
    len  <- Some(name.len());           // borrows `name` freely
    yield format!("{}: {} chars", name, len)
};
assert_eq!(result, Some("Alice: 5 chars".to_string()));

With Result

use cada::cada;
use cada::CadaError;

let result: Result<i32, CadaError> = cada! {
    x <- Ok::<_, std::io::Error>(10);
    y <- Ok::<_, std::io::Error>(20);
    yield x + y
};
assert_eq!(result.unwrap(), 30);

With collections

use cada::cada;

// Filter
let evens: Vec<i32> = cada! {
    x <- vec![1, 2, 3, 4, 5];
    guard x % 2 == 0;
    yield x
};
assert_eq!(evens, vec![2, 4]);

// Nested iteration (cross product)
let product: Vec<(i32, &str)> = cada! {
    x <- vec![1, 2];
    s <- vec!["a", "b"];
    yield (x, s)
};
assert_eq!(product, vec![(1, "a"), (1, "b"), (2, "a"), (2, "b")]);

With Chest (failure annotation)

use cada::prelude::*;
use cada::cada;

fn find_user(id: i32) -> Chest<String> {
    if id == 1 { Full("Alice".to_string()) } else { Chest::Empty }
}

let result: Chest<String> = cada! {
    user <- find_user(99) ?~ "user not found", 404u16;
    yield user
};

assert!(result.is_failure());
assert_eq!(result.failure_msg(), Some("user not found"));
assert_eq!(result.failure_param::<u16>(), Some(&404));

Mixing types

Different source types can be mixed freely. The first source determines the output type:

use cada::prelude::*;
use cada::cada;

fn validate(x: i32) -> Option<i32> {
    if x > 0 { Some(x) } else { None }
}

// Vec source with Option secondary -- filters out None results
let result: Vec<(i32, i32)> = cada! {
    x <- vec![1, -2, 3, -4];
    y <- validate(x);
    yield (x, y)
};
assert_eq!(result, vec![(1, 1), (3, 3)]);

Supported Types

Type As source (<-) As output (first source)
Chest<T> Yes Yes
Option<T> Yes Yes
Result<T, E> Yes Yes
Vec<T> Yes Yes
VecDeque<T> Yes Yes
LinkedList<T> Yes Yes
HashSet<T> Yes Yes
BTreeSet<T> Yes Yes
BinaryHeap<T> Yes Yes
HashMap<K, V> Yes Yes
BTreeMap<K, V> Yes Yes
JsonValue Yes Yes (→ Vec<Y>)

JsonValue requires the serde_json feature.

Features

  • serde -- enables Serialize/Deserialize for Chest<T>
  • serde_json -- enables LINQ-style JSON querying via JsonValue with typed extraction markers in cada! bindings
[dependencies]
cada = { version = "0.1", features = ["serde_json"] }

JSON Querying (serde_json feature)

The serde_json feature adds JsonValue, a wrapper around serde_json::Value that supports path navigation with / and integrates with cada! for typed extraction.

Path Navigation

use cada::prelude::*;

let v = JsonValue::new(serde_json::json!({"a": {"b": 42}}));
let inner = v / "a" / "b";
assert_eq!(inner.into_inner(), serde_json::json!(42));

// / distributes across arrays
let v = JsonValue::new(serde_json::json!([{"a": 1}, {"a": 2}, "skip"]));
let result = v / "a";
assert_eq!(result.into_inner(), serde_json::json!([1, 2]));

Typed Extraction in Comprehensions

Use name: Type <- expr syntax to perform type-filtered extraction:

use cada::prelude::*;
use cada::cada;

let data = serde_json::json!({
    "users": [
        {"name": "Alice", "age": 30},
        {"name": "Bob", "age": 25}
    ]
});

let names: Vec<String> = cada! {
    user: Array <- JsonValue::new(data) / "users";
    name: String <- user / "name";
    yield name
};
assert_eq!(names, vec!["Alice".to_string(), "Bob".to_string()]);

Available type markers:

Marker Extracts Rust type
Array JSON array elements JsonValue
String JSON string String
Number JSON number f64
Object JSON object JsonValue
Bool JSON boolean bool
Null JSON null ()
I64 JSON integer i64
U64 JSON unsigned integer u64
Isize JSON integer isize
Usize JSON unsigned integer usize
F64 JSON number (same as Number) f64

Non-matching values are silently skipped (zero iterations).

Extending

Implement CadaSource and CadaFirst for your own types to make them work inside cada!. See src/impls/ for examples.

Further Reading

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.