You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
🔧 Failure(E, L): failure carries accumulated loss — the cost of getting here
Failure now carries the loss that accumulated before it. A pipeline that
runs three Partial steps and fails on the fourth no longer drops the loss.
- Failure(E) → Failure(E, L) throughout
- .eh() bind combines Partial loss into Failure loss
- .compose() propagates loss through failure path
- Eh context accumulates Failure loss before returning Err
- .loss() on Failure returns carried loss (not L::total())
- .err_with_loss() → Option<(E, L)> for when you need both
- From<Result> / From<Option> use L::zero() for fresh failures
- Into<Result> drops loss (the price of going binary)
- 18 new tests for Failure(E, L) behavior
- All docs and examples updated
Co-Authored-By: Mara <mara@systemic.engineer>
Extracts the value from an `Imperfect`, accumulating any loss. Returns `Ok(T)` for Success and Partial, `Err(E)` for Failure.
54
54
55
-
This is where loss gets absorbed into the context. Success adds nothing. Partial adds its loss (via `combine` if loss already exists). Failure returns `Err` immediately.
55
+
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`.
Copy file name to clipboardExpand all lines: docs/pipeline.md
+8-6Lines changed: 8 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -55,24 +55,26 @@ assert!(result.is_partial());
55
55
assert_eq!(result.loss().steps(), 5); // max(3, 5) for ConvergenceLoss
56
56
```
57
57
58
-
### Anything x Failure = Failure
58
+
### Anything x Failure = Failure (loss carried)
59
59
60
-
Failure short-circuits. If the input is Failure, `f` is never called. If `f` returns Failure, prior loss is discarded — the value is gone.
60
+
Failure short-circuits. If the input is Failure, `f` is never called. If `f` returns Failure, prior loss is combined with the failure's loss — the value is gone, but the cost of getting here is measured.
Copy file name to clipboardExpand all lines: docs/terni-functor.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ A terni-functor is a three-state composition that carries a monoidal annotation
8
8
9
9
-**Success(T)** — pure value, zero annotation
10
10
-**Partial(T, L)** — value with annotation
11
-
-**Failure(E)** — no value
11
+
-**Failure(E, L)** — no value, but the cost of getting here is measured
12
12
13
13
The bind operator (`.eh()`) composes these while accumulating the annotation via the `Loss` monoid.
14
14
@@ -24,9 +24,9 @@ Haskell's `Writer w a` carries a monoidal log alongside a value. `Partial(T, L)`
24
24
25
25
-`Success` carries no log (it's structurally absent, not zero)
26
26
-`Partial` carries the log
27
-
-`Failure` has no value to log against
27
+
-`Failure` has no value, but carries the accumulated loss from before the failure
28
28
29
-
This is not `Writer`. `Writer` is `(a, w)`. `Imperfect` is `Success a | Partial a w | Failure e`. The failure path and the "genuinely zero loss" path both exist as distinct states, not as special values of the monoid.
29
+
This is not `Writer`. `Writer` is `(a, w)`. `Imperfect` is `Success a | Partial a w | Failure e w`. The failure path and the "genuinely zero loss" path both exist as distinct states, not as special values of the monoid. Failure carries loss to preserve the cost of computation that preceded it.
0 commit comments