accumulation — zipOrAccumulate & friends

Validation wants all the errors, not the first one. These operations run every branch and concatenate the failures into a NonEmptyList — Arrow 2.x's replacement for a separate Validated type.

on Raise<Nel<E>>: R accumulate<R>(R Function(Accumulator<E> acc) block) R zipOrAccumulate2..5(branch1, …, combine) List<B> mapOrAccumulate<A, B>(items, transform) · A bindNel<A>(EitherNel<E, A> e) on Accumulator<E>: accumulating(block) · dependent(block) · hasErrors

Lecture

Inside either<Nel<E>, _>(...) — any scope whose error type is a NonEmptyList — the scope gains the accumulation vocabulary:

The contract is Arrow's: every branch runs (errors concatenate in branch order), a branch that throws instead of raising wins over accumulation, and after the first error successful results are no longer retained — iteration continues only to collect the remaining errors.

Demo 1 · zipOrAccumulate2

Demo 2 · accumulate — the general form

Demo 3 · mapOrAccumulate, bindNel & toEitherNel

Demo 4 · dependent — rules that read sibling values

A branch cannot read a sibling's Accumulated.value without detonating — accumulation's one hard rule. But real validation has dependent rules ("an expense needs a positive amount"), which is why forms kept dropping down to a manual if (!acc.hasErrors) guard. acc.dependent(block) names that guard: the block runs only when every branch so far succeeded — so sibling .value reads inside it are safe by construction — and is skipped entirely otherwise. (No Arrow counterpart; Arrow users hand-roll the same guard.)

Try it yourself

Exercise: finish both branches of signup so the second call reports both failures.

Related: NonEmptyList — the error carrier · Either × pipelines — fail-slow validation over fx() chains, with concurrency · either & Raise — the fail-fast scope this extends · typed errors — full guide