terni 0.5.0

Ternary error handling: Success, Partial with measured loss, Failure. Because computation is not binary.
Documentation
# 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

```rust
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.

```rust
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).

```rust
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:

```rust
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

```rust
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`:

```rust
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
```

[Back to README](../README.md) · [Terni-functor →](terni-functor.md)