Skip to content

Latest commit

 

History

History
238 lines (172 loc) · 8.64 KB

File metadata and controls

238 lines (172 loc) · 8.64 KB

Context

The Eh struct is a composition context that accumulates loss across a sequence of Imperfect operations, converting each to Result.

Why

The .eh() pipeline is clean when every step returns Imperfect. But sometimes you need to interleave Imperfect and Result operations in the same function, or you need early return on failure. Eh bridges the two worlds.

Note: Imperfect does not implement the Try trait (it's nightly-only), so you can't use ? directly in functions returning Imperfect. Use match on the Result from eh.eh() and return Imperfect::Failure on Err.

Basic usage

use terni::{Imperfect, Eh, ConvergenceLoss};

fn process() -> Imperfect<i32, String, ConvergenceLoss> {
    let mut eh = Eh::new();

    let a = match eh.eh(Imperfect::<i32, String, ConvergenceLoss>::Success(10)) {
        Ok(v) => v,
        Err(e) => return Imperfect::Failure(e, ConvergenceLoss::zero()),
    };

    let b = match eh.eh(Imperfect::<_, String, _>::Partial(a + 5, ConvergenceLoss::new(3))) {
        Ok(v) => v,
        Err(e) => return Imperfect::Failure(e, ConvergenceLoss::zero()),
    };

    // If any step was Failure, we already returned.
    // If any step was Partial, loss is accumulated in eh.
    eh.finish(b)
}

# let result = process();
# assert!(result.is_partial());

API

Eh::new()

Creates a context with zero accumulated loss.

use terni::{Eh, ConvergenceLoss};

let eh: Eh<ConvergenceLoss> = Eh::new();
assert!(eh.loss().is_none());

.eh(imp) -> Result<T, E>

Extracts the value from an Imperfect, accumulating any loss. Returns Ok(T) for Success and Partial, Err(E) for Failure.

This is where loss gets absorbed into the context. Success adds nothing. Partial adds its loss (via combine if loss already exists). Failure accumulates its carried loss into the context, then returns Err. Since Failure(E, L) carries accumulated loss, that loss is combined into the context before the error is returned.

.imp() and .tri()

Aliases for .eh(), same as on Imperfect itself.

.loss() -> Option<&L>

Inspect accumulated loss without consuming the context. Returns None if no loss has accumulated (all steps were Success).

use terni::{Imperfect, Eh, ConvergenceLoss};

let mut eh: Eh<ConvergenceLoss> = Eh::new();
assert!(eh.loss().is_none());

let _ = eh.eh(Imperfect::<i32, String, ConvergenceLoss>::Partial(1, ConvergenceLoss::new(3)));
assert_eq!(eh.loss().unwrap().steps(), 3);

let _ = eh.eh(Imperfect::<i32, String, ConvergenceLoss>::Partial(2, ConvergenceLoss::new(7)));
assert_eq!(eh.loss().unwrap().steps(), 7);  // max(3, 7)

.finish(value) -> Imperfect<T, E, L>

Wraps the final value with accumulated loss. If no loss accumulated, returns Success. If any did, returns Partial.

This is the exit point. It converts back from Result-land to Imperfect.

#[must_use]

Eh is marked #[must_use]. If you create an Eh and drop it without calling .finish(), the compiler warns you. Dropping the context silently discards accumulated loss — exactly the information Imperfect exists to preserve.

Mixing Imperfect and Result

Eh is the bridge between Imperfect and Result. Inside an Eh block, you can freely mix both:

use terni::{Imperfect, Eh, ConvergenceLoss};
use std::num::ParseIntError;

fn parse_and_validate(input: &str) -> Imperfect<i32, String, ConvergenceLoss> {
    let mut eh = Eh::new();

    // Result operation — parse the input
    let raw: i32 = match input.parse::<i32>() {
        Ok(n) => n,
        Err(e) => return Imperfect::Failure(e.to_string(), ConvergenceLoss::zero()),
    };

    // Imperfect operation — validate range
    let validated = match eh.eh(if raw > 100 {
        Imperfect::Partial(100, ConvergenceLoss::new(1))  // clamped
    } else if raw < 0 {
        Imperfect::<_, String, _>::Failure("negative".into(), ConvergenceLoss::zero())
    } else {
        Imperfect::Success(raw)
    }) {
        Ok(v) => v,
        Err(e) => return Imperfect::Failure(e, ConvergenceLoss::zero()),
    };

    // Another Result operation
    let doubled = match validated.checked_mul(2) {
        Some(v) => v,
        None => return Imperfect::Failure("overflow".to_string(), ConvergenceLoss::zero()),
    };

    eh.finish(doubled)
}

# let r = parse_and_validate("50");
# assert_eq!(r.ok(), Some(100));

The key insight: Eh.eh() returns Result, so you can match on it for early return. Loss accumulates only through Eh.eh() calls. Everything else is standard Rust error handling. If your function returns Result (not Imperfect), you can use ? on eh.eh() directly.

Example: payment verification

use terni::{Imperfect, Eh, ConvergenceLoss};

struct Payment { amount: u64, currency: String }
struct VerifiedPayment { amount: u64, currency: String, risk_score: f64 }

fn verify_amount(p: &Payment) -> Imperfect<u64, String, ConvergenceLoss> {
    if p.amount == 0 {
        Imperfect::Failure("zero amount".into(), ConvergenceLoss::zero())
    } else if p.amount > 10_000 {
        Imperfect::Partial(p.amount, ConvergenceLoss::new(2))  // needs review
    } else {
        Imperfect::Success(p.amount)
    }
}

fn verify_currency(c: &str) -> Imperfect<String, String, ConvergenceLoss> {
    match c {
        "USD" | "EUR" => Imperfect::Success(c.to_string()),
        "GBP" => Imperfect::Partial(c.to_string(), ConvergenceLoss::new(1)),  // supported but slower
        _ => Imperfect::Failure(format!("unsupported currency: {}", c), ConvergenceLoss::zero()),
    }
}

fn verify_payment(p: Payment) -> Imperfect<VerifiedPayment, String, ConvergenceLoss> {
    let mut eh = Eh::new();

    let amount = match eh.eh(verify_amount(&p)) {
        Ok(v) => v,
        Err(e) => return Imperfect::Failure(e, ConvergenceLoss::zero()),
    };
    let currency = match eh.eh(verify_currency(&p.currency)) {
        Ok(v) => v,
        Err(e) => return Imperfect::Failure(e, ConvergenceLoss::zero()),
    };

    let risk_score = match eh.loss() {
        Some(loss) => 0.5 + (loss.steps() as f64 * 0.1),  // higher loss = higher risk
        None => 0.1,
    };

    eh.finish(VerifiedPayment { amount, currency, risk_score })
}

# let p = Payment { amount: 15_000, currency: "GBP".into() };
# let result = verify_payment(p);
# assert!(result.is_partial());
# assert_eq!(result.loss().steps(), 2);  // max(2, 1)

The loss tells downstream consumers how much confidence to place in this result. Zero loss = fully verified. Nonzero = verified with caveats. Failure = rejected.

Recovery after early return

When Eh.eh() returns Err, you've already early-returned from the function. But the caller can recover using .recover() on the returned Imperfect:

use terni::{Imperfect, ConvergenceLoss};

fn fragile_step(x: i32) -> Imperfect<i32, String, ConvergenceLoss> {
    if x > 10 {
        Imperfect::Failure("too big".into(), ConvergenceLoss::new(2))
    } else {
        Imperfect::Success(x)
    }
}

// The pipeline fails, but we recover with a default
let result = Imperfect::<i32, String, ConvergenceLoss>::Success(20)
    .eh(fragile_step)
    .recover(|_e| Imperfect::Success(10));

// Recovery from Failure always produces Partial — the failure was real
assert!(result.is_partial());
assert_eq!(result.ok(), Some(10));
assert_eq!(result.loss().steps(), 2);  // the cost of getting here survives

Why prism-core doesn't use Eh

prism-core's Beam trait IS its own composition context. The tick primitive destructures the beam's Imperfect to extract inner values for building new beam structs — a pattern that requires direct match arms rather than Eh's Result-based extraction.

From core/src/beam.rs:

fn tick<T, NE>(self, next: Imperfect<T, NE, L>) -> PureBeam<Out, T, NE, L> {
    match self.imperfect {
        Imperfect::Failure(_, _) => panic!("tick on Err beam — check is_ok() first"),
        Imperfect::Success(old_out) => PureBeam {
            input: old_out,
            imperfect: next,
        },
        Imperfect::Partial(old_out, loss) => PureBeam {
            input: old_out,
            imperfect: propagate(loss, next),
        },
    }
}

The beam needs old_out to construct the new struct's input field. Eh.eh() would discard that inner value into a Result<T, E>, losing the structural information beam needs.

This is the right pattern: Eh is for code that consumes Imperfect values and produces a final result. Beam is for code that builds new Imperfect values from old ones while carrying structural context. Different layers, different tools.

Back to README · Terni-functor →