terni 0.5.0

Ternary error handling: Success, Partial with measured loss, Failure. Because computation is not binary.
Documentation
  • Coverage
  • 100%
    49 out of 49 items documented1 out of 46 items with examples
  • Size
  • Source code size: 153.22 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 4.48 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 1m 32s Average build duration of successful builds.
  • all releases: 1m 24s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • systemic-engineering/prism
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • alexocode

terni

I wanna thank Brene Brown for her work.

Ternary error handling for Rust. Because computation is not binary.

crates.io docs.rs license

The cost of honesty is 0.65 nanoseconds per step, only when there's something to be honest about. Otherwise: zero.

eh

The type. Three states instead of two.

use terni::{Imperfect, ConvergenceLoss};

let perfect: Imperfect<u32, String, ConvergenceLoss> = Imperfect::Success(42);
let lossy = Imperfect::Partial(42, ConvergenceLoss::new(3));
let failed: Imperfect<u32, String, ConvergenceLoss> = Imperfect::Failure("gone".into(), ConvergenceLoss::new(0));
let costly_failure: Imperfect<u32, String, ConvergenceLoss> = Imperfect::Failure("gone".into(), ConvergenceLoss::new(5));

assert!(perfect.is_ok());
assert!(lossy.is_partial());
assert!(failed.is_err());
// Failure carries accumulated loss — the cost of getting here:
assert_eq!(costly_failure.loss().steps(), 5);

Failure(E, L) carries the accumulated loss from before the failure. The loss tells you what it cost to arrive here. If you need to distinguish "failed immediately" from "failed after expensive work," the carried loss is that information.

Loss measures what didn't survive. It's a monoid: zero() identity, combine associative, total() absorbing.

Three loss types ship with the crate:

  • ConvergenceLoss — distance to crystal. Combine: max.
  • ApertureLoss — dark dimensions. Combine: union.
  • RoutingLoss — decision entropy. Combine: max entropy, min gap.

Migration from Result

Result terni
Ok(v) Imperfect::Success(v) same
Err(e) Imperfect::Failure(e, l) same
Imperfect::Partial(v, l) new
Imperfect::Failure(e, l) honest

The two empty cells on the left are the argument. Result doesn't have a row for partial success or honest failure. That's why terni exists.

Constructors

Four ways to build an Imperfect:

use terni::{Imperfect, ConvergenceLoss};

let a = Imperfect::<i32, String, ConvergenceLoss>::success(42);
let b = Imperfect::<i32, String, ConvergenceLoss>::partial(42, ConvergenceLoss::new(3));
let c = Imperfect::<i32, String, ConvergenceLoss>::failure("gone".into());
let d = Imperfect::<i32, String, ConvergenceLoss>::failure_with_loss("gone".into(), ConvergenceLoss::new(5));

.failure() carries zero loss. .failure_with_loss() carries accumulated loss from prior steps.

Loss types in depth → · Full migration guide →

eh!

The bind. Chain operations, accumulate loss.

use terni::{Imperfect, ConvergenceLoss};

let result = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .eh(|x| Imperfect::Success(x * 2))
    .eh(|x| Imperfect::Partial(x + 1, ConvergenceLoss::new(3)));

assert_eq!(result.ok(), Some(3));
assert!(result.is_partial());

Recovery from failure carries the cost:

use terni::{Imperfect, ConvergenceLoss};

let result = Imperfect::<i32, String, ConvergenceLoss>::Success(1)
    .eh(|x| Imperfect::Partial(x * 2, ConvergenceLoss::new(3)))
    .eh(|_| Imperfect::<i32, String, ConvergenceLoss>::Failure("broke".into(), ConvergenceLoss::new(2)))
    .recover(|_e| Imperfect::Success(0));

// Recovery from Failure always produces Partial — the failure was real
assert!(result.is_partial());
assert_eq!(result.ok(), Some(0));

For explicit context with loss accumulation:

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

let mut eh = Eh::new();
let a = eh.eh(Imperfect::<i32, String, ConvergenceLoss>::Success(1)).unwrap();
let b = eh.eh(Imperfect::<_, String, _>::Partial(a + 1, ConvergenceLoss::new(5))).unwrap();
let result: Imperfect<i32, String, ConvergenceLoss> = eh.finish(b);

assert!(result.is_partial());

.imp() and .tri() are aliases for .eh() — same bind, different name. Use whichever reads best in your code.

Pipeline guide → · Context guide →

eh?

The question. Coming in a future release.

Block macro for implicit loss accumulation — eh! { } will do what Eh does without the boilerplate.

More

  • Loss types — the Loss trait, shipped types, stdlib impls, custom implementations
  • Pipeline.eh() bind in depth, loss accumulation rules
  • ContextEh struct, mixing Imperfect and Result
  • Terni-functor — the math behind .eh()
  • Migration — moving from Result<T, E> to Imperfect<T, E, L>
  • Flight recorderFailure(E, L) as production telemetry, not stack traces
  • Benchmarks — 0.65 ns per honest step, zero on the success path

License

Apache-2.0